├── .github ├── dependabot.yml └── workflows │ └── main.yml ├── .golangci.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── box2d.go ├── box2d_test.go ├── box3d.go ├── bufferparams.go ├── bufferparams_test.go ├── context.go ├── context_test.go ├── coordseq.go ├── coordseq_test.go ├── defaultcontext.go ├── examples ├── go.mod ├── go.sum └── postgis │ ├── README.md │ ├── main.go │ └── main_test.go ├── geojson ├── geojson.go └── geojson_test.go ├── geom.go ├── geom_test.go ├── geometry ├── binary.go ├── binary_test.go ├── bounds_test.go ├── geojson.go ├── geojson_test.go ├── geometry.go ├── geometry_test.go ├── gob.go ├── gob_test.go ├── kml.go ├── kml_test.go ├── sql.go ├── sql_test.go ├── text.go ├── text_test.go └── util_test.go ├── geommethods.go ├── geommethods.go.tmpl ├── geommethods.yaml ├── geos.go ├── geos_test.go ├── go-geos.c ├── go-geos.h ├── go.mod ├── go.sum ├── internal ├── cmds │ └── execute-template │ │ └── main.go └── scripts │ └── guess-missing-methods.sh ├── prepgeom.go ├── prepgeom_test.go ├── strtree.go └── strtree_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: monthly 7 | labels: 8 | - enhancement 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: monthly 13 | labels: 14 | - enhancement 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | tags: 8 | - v* 9 | env: 10 | GOTOOLCHAIN: local 11 | jobs: 12 | main: 13 | runs-on: ubuntu-22.04 14 | steps: 15 | - name: install-dependencies 16 | run: | 17 | sudo apt-get install -y libgeos-dev 18 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 19 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 20 | with: 21 | go-version: stable 22 | - name: generate 23 | run: | 24 | go generate ./... 25 | git diff --exit-code 26 | - name: build 27 | run: go build ./... 28 | - name: test 29 | run: go test -race ./... 30 | - name: test-examples 31 | run: ( cd examples && go test -race ./... ) 32 | lint: 33 | runs-on: ubuntu-22.04 34 | steps: 35 | - name: install-dependencies 36 | run: | 37 | sudo apt-get install -y libgeos-dev 38 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 39 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 40 | with: 41 | go-version: stable 42 | - uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 43 | with: 44 | version: v2.1.6 45 | geos-versions: 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | geos-version: 50 | - 3.10.2 # Used in Ubuntu 22.04 LTS 51 | - 3.10.7 # Latest 3.10.x 52 | - 3.11.5 # Latest 3.11.x 53 | - 3.12.1 # Used in Ubuntu 24.04 LTS 54 | - 3.12.3 # Latest 3.12.x 55 | - 3.13.1 # Latest 56 | runs-on: ubuntu-22.04 57 | steps: 58 | - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 59 | id: cache-geos 60 | with: 61 | path: ~/work/geos-${{ matrix.geos-version }} 62 | key: ${{ runner.os }}-geos-${{ matrix.geos-version }} 63 | - name: build-geos 64 | if: ${{ steps.cache-geos.outputs.cache-hit != 'true' }} 65 | run: | 66 | cd ~/work 67 | curl https://download.osgeo.org/geos/geos-${{ matrix.geos-version }}.tar.bz2 | tar xjf - 68 | cd geos-${{ matrix.geos-version }} 69 | mkdir _build 70 | cd _build 71 | cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_POLICY_VERSION_MINIMUM=3.5 .. 72 | make -j4 73 | ctest 74 | - name: install-geos 75 | run: | 76 | cd ~/work/geos-${{ matrix.geos-version }}/_build 77 | sudo make install 78 | - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 79 | with: 80 | go-version: stable 81 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 82 | - name: test 83 | run: | 84 | sudo ldconfig 85 | go test ./... -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | run: 3 | go: '1.24' 4 | linters: 5 | enable: 6 | - asciicheck 7 | - bidichk 8 | - bodyclose 9 | - canonicalheader 10 | - containedctx 11 | - copyloopvar 12 | - decorder 13 | - dogsled 14 | - dupword 15 | - durationcheck 16 | - err113 17 | - errchkjson 18 | - errname 19 | - errorlint 20 | - exptostd 21 | - fatcontext 22 | - forbidigo 23 | - forcetypeassert 24 | - funcorder 25 | - gocheckcompilerdirectives 26 | - gochecksumtype 27 | - gocritic 28 | - godot 29 | - gomodguard 30 | - goprintffuncname 31 | - gosmopolitan 32 | - grouper 33 | - iface 34 | - importas 35 | - inamedparam 36 | - interfacebloat 37 | - intrange 38 | - loggercheck 39 | - makezero 40 | - mirror 41 | - misspell 42 | - nilerr 43 | - nilnesserr 44 | - noctx 45 | - nolintlint 46 | - nosprintfhostport 47 | - perfsprint 48 | - prealloc 49 | - predeclared 50 | - promlinter 51 | - protogetter 52 | - reassign 53 | - revive 54 | - rowserrcheck 55 | - sloglint 56 | - spancheck 57 | - sqlclosecheck 58 | - staticcheck 59 | - tagalign 60 | - tagliatelle 61 | - testableexamples 62 | - testifylint 63 | - thelper 64 | - unconvert 65 | - unparam 66 | - usestdlibvars 67 | - usetesting 68 | - wastedassign 69 | - whitespace 70 | - zerologlint 71 | disable: 72 | - asasalint 73 | - contextcheck 74 | - cyclop 75 | - depguard 76 | - dupl 77 | - exhaustive 78 | - exhaustruct 79 | - funlen 80 | - ginkgolinter 81 | - gochecknoglobals 82 | - gochecknoinits 83 | - gocognit 84 | - goconst 85 | - gocyclo 86 | - godox 87 | - goheader 88 | - gomoddirectives 89 | - gosec 90 | - ireturn 91 | - lll 92 | - maintidx 93 | - musttag 94 | - nakedret 95 | - nestif 96 | - nilnil 97 | - nlreturn 98 | - nonamedreturns 99 | - paralleltest 100 | - recvcheck 101 | - testpackage 102 | - tparallel 103 | - varnamelen 104 | - wrapcheck 105 | - wsl 106 | settings: 107 | forbidigo: 108 | exclude-godoc-examples: true 109 | analyze-types: true 110 | gocritic: 111 | enable-all: true 112 | disabled-checks: 113 | - dupImport 114 | - emptyFallthrough 115 | - hugeParam 116 | - rangeValCopy 117 | - unnamedResult 118 | - whyNoLint 119 | govet: 120 | disable: 121 | - fieldalignment 122 | - shadow 123 | enable-all: true 124 | misspell: 125 | locale: US 126 | ignore-rules: 127 | - mitre 128 | revive: 129 | enable-all-rules: true 130 | rules: 131 | - name: add-constant 132 | disabled: true 133 | - name: call-to-gc 134 | disabled: true 135 | - name: cognitive-complexity 136 | disabled: true 137 | - name: cyclomatic 138 | disabled: true 139 | - name: empty-block 140 | disabled: true 141 | - name: exported 142 | disabled: true 143 | - name: filename-format 144 | arguments: 145 | - ^[a-z][-0-9_a-z]*(?:\.gen)?\.go$ 146 | - name: flag-parameter 147 | disabled: true 148 | - name: function-length 149 | disabled: true 150 | - name: function-result-limit 151 | disabled: true 152 | - name: import-shadowing 153 | disabled: true 154 | - name: line-length-limit 155 | disabled: true 156 | - name: max-control-nesting 157 | disabled: true 158 | - name: max-public-structs 159 | disabled: true 160 | - name: nested-structs 161 | disabled: true 162 | - name: unused-parameter 163 | disabled: true 164 | - name: unused-receiver 165 | disabled: true 166 | staticcheck: 167 | checks: 168 | - all 169 | exclusions: 170 | generated: lax 171 | presets: 172 | - common-false-positives 173 | - legacy 174 | - std-error-handling 175 | rules: 176 | - linters: 177 | - err113 178 | text: do not define dynamic errors, use wrapped static errors instead 179 | - linters: 180 | - forbidigo 181 | path: ^internal/cmds/ 182 | - linters: 183 | - forcetypeassert 184 | path: _test\.go$ 185 | - linters: 186 | - forbidigo 187 | path: assets/scripts/generate-commit.go 188 | formatters: 189 | enable: 190 | - gci 191 | - gofmt 192 | - gofumpt 193 | - goimports 194 | - golines 195 | settings: 196 | gci: 197 | sections: 198 | - standard 199 | - default 200 | - prefix(github.com/twpayne/go-geos) 201 | gofumpt: 202 | module-path: github.com/twpayne/go-geos 203 | extra-rules: true 204 | goimports: 205 | local-prefixes: 206 | - github.com/twpayne/go-geos 207 | golines: 208 | max-len: 256 209 | tab-len: 4 210 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | `go-geos` uses `libgeos`'s stable C API, 4 | [`geos_c.h`](http://libgeos.org/doxygen/geos__c_8h.html). Only the thread-safe 5 | `*_r` functions are used. 6 | 7 | ## Adding methods to `*Geom` 8 | 9 | Wherever possible, `go-geos` uses code generation to generate wrappers for 10 | `*Geom` methods. The generated code is in 11 | [`geommethods.go`](https://github.com/twpayne/go-geos/blob/master/geommethods.go). 12 | 13 | There are five parts to this: 14 | 15 | * [`geommethods.yaml`](https://github.com/twpayne/go-geos/blob/master/geommethods.yaml) 16 | contains the high-level definitions of the methods. 17 | * [`geommethods.go.tmpl`](https://github.com/twpayne/go-geos/blob/master/geommethods.go.tmpl) 18 | is a `text/template` template that is executed with the data from 19 | `geommethods.yaml`. 20 | * [`internal/cmds/execute-template/`](https://github.com/twpayne/go-geos/tree/master/internal/cmds/execute-template) 21 | executes a template with data and includes custom template functions. 22 | * [`go generate`](https://go.dev/blog/generate) runs 23 | `internal/cmds/execute-template/` with `geommethods.yaml` and 24 | `geommethods.go.tmpl` as inputs and writes `geommethods.go`. 25 | * [`geom_test.go`](https://github.com/twpayne/go-geos/blob/master/geom_test.go) 26 | contains unit tests to ensure that the method is wrapped correctly. 27 | 28 | Adding a method to `*Geom` consists of one or more steps, depending on how 29 | similar the method is to existing methods: 30 | 31 | 1. In simple cases, adding a few lines to `geommethods.yaml` and running `go 32 | generate` is sufficient. You will need to add a test to `geom_test.go`. 33 | 2. For more complex cases, you might have to modify or extend 34 | `geommethods.go.tmpl`. 35 | 3. If you need to add or modify a template function, you will need to modify 36 | `internal/cmds/execute-template/`. 37 | 38 | ## Maintaining backwards compatibility 39 | 40 | `go-geos` supports the libgeos version using in the latest [Ubuntu LTS 41 | release](https://ubuntu.com/about/release-cycle), which is currently GEOS 42 | 3.10.2. 43 | 44 | As `libgeos` is under active development, bugs are fixed and new features are 45 | added over time. This causes problems when versions might behave incorrectly or 46 | miss newly-added features. In these cases: 47 | 48 | * In general, it is the user's responsibility to ensure that they are using a 49 | sufficiently recent version of `libgeos` for their needs. `go-geos` can 50 | forward incorrect results from `libgeos` and behave in an undefined manner 51 | (including crashing the program) when missing features are invoked. 52 | * For features not present in GEOS 3.10.2, you will need to add stubs in 53 | [`go-geos.c`](https://github.com/twpayne/go-geos/blob/master/go-geos.c) and 54 | [`go-geos.h`](https://github.com/twpayne/go-geos/blob/master/go-geos.h) to 55 | provide the function when it is not provided. 56 | * [`VersionCompare`](https://pkg.go.dev/github.com/twpayne/go-geos#VersionCompare) 57 | can be used in tests for the CI to pass on all versions. 58 | 59 | ## C code formatting 60 | 61 | `go-geos` uses [`clang-format`](https://clang.llvm.org/docs/ClangFormat.html) to 62 | format C code. You can run this with: 63 | 64 | ```console 65 | $ clang-format -i *.c *.h 66 | ``` 67 | 68 | ## Go code linting 69 | 70 | `go-geos` uses [`golangci-lint`](https://golangci-lint.run/) to lint Go code. 71 | You can run it with: 72 | 73 | ``` 74 | $ golangci-lint run 75 | ``` 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Tom Payne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-geos 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/twpayne/go-geos)](https://pkg.go.dev/github.com/twpayne/go-geos) 4 | 5 | Package `go-geos` provides an interface to [GEOS](https://libgeos.org). 6 | 7 | ## Install 8 | 9 | ```console 10 | $ go get github.com/twpayne/go-geos 11 | ``` 12 | 13 | You must also install the GEOS development headers and libraries. These are 14 | typically in the package `libgeos-dev` on Debian-like systems, `geos-devel` on 15 | RedHat-like systems, and `geos` in Homebrew. 16 | 17 | ## Features 18 | 19 | * Fluent Go API. 20 | 21 | * Low-level `Context`, `CoordSeq`, `Geom`, `PrepGeom`, and `STRtree` types 22 | provide access to all GEOS methods. 23 | 24 | * High-level `geometry.Geometry` type implements all GEOS functionality and 25 | many standard Go interfaces: 26 | 27 | * `database/sql/driver.Valuer` and `database/sql.Scanner` (WKB) for PostGIS 28 | database integration. 29 | * `encoding/json.Marshaler` and `encoding/json.Unmarshaler` (GeoJSON). 30 | * `encoding/xml.Marshaler` (KML). 31 | * `encoding.BinaryMarshaler` and `encoding.BinaryUnmarshaler` (WKB). 32 | * `encoding.TextMarshaler` and `encoding.TextUnmarshaler` (WKT). 33 | * `encoding/gob.GobEncoder` and `encoding/gob.GobDecoder` (GOB). 34 | 35 | See the [PostGIS example](examples/postgis/README.md) for a demonstration of 36 | the use of these interfaces. 37 | 38 | * Concurrency-safe. `go-geos` uses GEOS's threadsafe `*_r` functions under the 39 | hood, with locking to ensure safety, even when used across multiple 40 | goroutines. For best performance, use one `geos.Context` per goroutine. 41 | 42 | * Caching of geometry properties to avoid cgo overhead. 43 | 44 | * Optimized GeoJSON encoder. 45 | 46 | * Automatic finalization of GEOS objects. 47 | 48 | ## Memory management 49 | 50 | `go-geos` objects live mostly on the C heap. `go-geos` sets finalizers on the 51 | objects it creates that free the associated C memory. However, the C heap is not 52 | visible to the Go runtime. The can result in significant memory pressure as 53 | memory is consumed by large, non-finalized geometries, of which the Go runtime 54 | is unaware. Consequently, if it is known that a geometry will no longer be used, 55 | it should be explicitly freed by calling its `Destroy()` method. Periodic calls 56 | to `runtime.GC()` can also help, but the Go runtime makes no guarantees about 57 | when or if finalizers will be called. 58 | 59 | You can set a function to be called whenever a geometry's finalizer is invoked 60 | with the `WithGeomFinalizeFunc` option to `NewContext()`. This can be helpful 61 | for tracking down geometry leaks. 62 | 63 | For more information, see the [documentation for 64 | `runtime.SetFinalizer()`](https://pkg.go.dev/runtime#SetFinalizer) and [this 65 | thread on 66 | `golang-nuts`](https://groups.google.com/g/golang-nuts/c/XnV16PxXBfA/m/W8VEzIvHBAAJ). 67 | 68 | ## Errors, exceptions, and panics 69 | 70 | `go-geos` uses the stable GEOS C bindings. These bindings catch exceptions from 71 | the underlying C++ code and convert them to an integer return code. For normal 72 | geometry operations, `go-geos` panics whenever it encounters a GEOS return code 73 | indicating an error, rather than returning an `error`. Such panics will not 74 | occur if `go-geos` is used correctly. Panics will occur for invalid API calls, 75 | out-of-bounds access, or operations on invalid geometries. This behavior is 76 | similar to slice access in Go (out-of-bounds accesses panic) and keeps the API 77 | fluent. When parsing data, errors are expected so an `error` is returned. 78 | 79 | ## Comparison with `github.com/twpayne/go-geom` 80 | 81 | [`github.com/twpayne/go-geom`](https://github.com/twpayne/go-geom) is a pure Go 82 | library providing similar functionality to `go-geos`. The major differences are: 83 | 84 | * `go-geos` uses [GEOS](https://libgeos.org), which is an extremely mature 85 | library with a rich feature set. 86 | * `go-geos` uses cgo, with all the disadvantages that that entails, notably 87 | expensive function call overhead, more complex memory management and trickier 88 | cross-compilation. 89 | * `go-geom` uses a cache-friendly coordinate layout which is generally faster 90 | than GEOS for many operations. 91 | 92 | `go-geos` is a good fit if your program is short-lived (meaning you can ignore 93 | memory management), or you require the battle-tested geometry functions provided 94 | by GEOS and are willing to handle memory management manually. `go-geom` is 95 | recommended for long-running processes with less stringent geometry function 96 | requirements. 97 | 98 | ## GEOS version compatibility 99 | 100 | `go-geos` is tested to work with the versions of `GEOS` tested on CI. 101 | See [here](.github/workflows/main.yml). 102 | 103 | Calling functions unsupported by the underlying `GEOS` library will result in a panic. 104 | Users can use [`VersionCompare`](https://pkg.go.dev/github.com/twpayne/go-geos#VersionCompare) 105 | to be sure that a function exists. 106 | 107 | ## Contributing 108 | 109 | Please check [`CONTRIBUTING.md`](./CONTRIBUTING.md) for instructions before you open a pull-request! 110 | 111 | ## License 112 | 113 | MIT 114 | -------------------------------------------------------------------------------- /box2d.go: -------------------------------------------------------------------------------- 1 | package geos 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // A Box2D is a two-dimensional bounds. 9 | type Box2D struct { 10 | MinX float64 11 | MinY float64 12 | MaxX float64 13 | MaxY float64 14 | } 15 | 16 | // NewBox2D returns a new bounds. 17 | func NewBox2D(minX, minY, maxX, maxY float64) *Box2D { 18 | return &Box2D{ 19 | MinX: minX, 20 | MinY: minY, 21 | MaxX: maxX, 22 | MaxY: maxY, 23 | } 24 | } 25 | 26 | // NewBox2DEmpty returns a new empty bounds. 27 | func NewBox2DEmpty() *Box2D { 28 | return &Box2D{ 29 | MinX: math.Inf(1), 30 | MinY: math.Inf(1), 31 | MaxX: math.Inf(-1), 32 | MaxY: math.Inf(-1), 33 | } 34 | } 35 | 36 | // Contains returns true if b contains other. 37 | func (b *Box2D) Contains(other *Box2D) bool { 38 | if b.IsEmpty() || other.IsEmpty() { 39 | return false 40 | } 41 | return other.MinX >= b.MinX && other.MinY >= b.MinY && other.MaxX <= b.MaxX && other.MaxY <= b.MaxY 42 | } 43 | 44 | // ContainsPoint returns true if b contains the point at x, y. 45 | func (b *Box2D) ContainsPoint(x, y float64) bool { 46 | return b.MinX <= x && x <= b.MaxX && b.MinY <= y && y <= b.MaxY 47 | } 48 | 49 | // ContextGeom returns b as a Geom. 50 | func (b *Box2D) ContextGeom(context *Context) *Geom { 51 | return context.NewGeomFromBounds(b.MinX, b.MinY, b.MaxX, b.MaxY) 52 | } 53 | 54 | // Equals returns true if b equals other. 55 | func (b *Box2D) Equals(other *Box2D) bool { 56 | return b.MinX == other.MinX && b.MinY == other.MinY && b.MaxX == other.MaxX && b.MaxY == other.MaxY 57 | } 58 | 59 | // Geom returns b as a Geom. 60 | func (b *Box2D) Geom() *Geom { 61 | return b.ContextGeom(DefaultContext) 62 | } 63 | 64 | // IsEmpty returns true if b is empty. 65 | func (b *Box2D) IsEmpty() bool { 66 | return b.MinX > b.MaxX || b.MinY > b.MaxY 67 | } 68 | 69 | // Height returns the height of b. 70 | func (b *Box2D) Height() float64 { 71 | return b.MaxY - b.MinY 72 | } 73 | 74 | // Intersects returns true if b intersects other. 75 | func (b *Box2D) Intersects(other *Box2D) bool { 76 | return !(other.MinX > b.MaxX || other.MinY > b.MaxY || other.MaxX < b.MinX || other.MaxY < b.MinY) 77 | } 78 | 79 | // IsPoint returns true if b is a point. 80 | func (b *Box2D) IsPoint() bool { 81 | return b.MinX == b.MaxX && b.MinY == b.MaxY 82 | } 83 | 84 | func (b *Box2D) String() string { 85 | return fmt.Sprintf("[%f %f %f %f]", b.MinX, b.MinY, b.MaxX, b.MaxY) 86 | } 87 | 88 | // Width returns the width of b. 89 | func (b *Box2D) Width() float64 { 90 | return b.MaxX - b.MinX 91 | } 92 | -------------------------------------------------------------------------------- /box2d_test.go: -------------------------------------------------------------------------------- 1 | package geos_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | 8 | "github.com/twpayne/go-geos" 9 | ) 10 | 11 | func TestBox2D(t *testing.T) { 12 | b := geos.NewBox2D(1, 2, 3, 4) 13 | assert.True(t, b.Contains(b)) 14 | assert.True(t, b.Contains(geos.NewBox2D(1.5, 2.5, 2.5, 3.5))) 15 | assert.False(t, b.Contains(geos.NewBox2D(1.5, 0.5, 2.5, 1.5))) 16 | assert.True(t, b.ContainsPoint(2, 3)) 17 | assert.False(t, b.ContainsPoint(2, 1)) 18 | assert.True(t, b.Equals(geos.NewBox2D(1, 2, 3, 4))) 19 | expectedWKT := "POLYGON ((1 2, 3 2, 3 4, 1 4, 1 2))" 20 | if geos.VersionCompare(3, 12, 0) < 0 { 21 | expectedWKT = "POLYGON ((1.0000000000000000 2.0000000000000000, 3.0000000000000000 2.0000000000000000, 3.0000000000000000 4.0000000000000000, 1.0000000000000000 4.0000000000000000, 1.0000000000000000 2.0000000000000000))" 22 | } 23 | assert.Equal(t, expectedWKT, b.Geom().ToWKT()) 24 | assert.False(t, b.IsEmpty()) 25 | assert.Equal(t, 2.0, b.Height()) 26 | assert.True(t, b.Intersects(b)) 27 | assert.True(t, b.Intersects(geos.NewBox2D(1.5, 2.5, 2.5, 3.5))) 28 | assert.True(t, b.Intersects(geos.NewBox2D(1.5, 0.5, 2.5, 3.5))) 29 | assert.False(t, b.Intersects(geos.NewBox2D(1.5, 0.5, 2.5, 1.5))) 30 | assert.False(t, b.IsPoint()) 31 | assert.Equal(t, 2.0, b.Width()) 32 | } 33 | 34 | func TestBox2DEmpty(t *testing.T) { 35 | b := geos.NewBox2DEmpty() 36 | assert.False(t, b.Contains(b)) 37 | assert.False(t, b.Contains(geos.NewBox2DEmpty())) 38 | assert.False(t, b.ContainsPoint(0, 0)) 39 | assert.True(t, b.Equals(b)) //nolint:gocritic 40 | assert.Equal(t, "POINT EMPTY", b.Geom().ToWKT()) 41 | assert.True(t, b.IsEmpty()) 42 | assert.False(t, b.Intersects(b)) 43 | assert.False(t, b.IsPoint()) 44 | } 45 | 46 | func TestBox2DPoint(t *testing.T) { 47 | b := geos.NewBox2D(0, 0, 0, 0) 48 | assert.True(t, b.Contains(b)) 49 | assert.False(t, b.Contains(geos.NewBox2D(1, 2, 3, 4))) 50 | assert.True(t, b.ContainsPoint(0, 0)) 51 | assert.False(t, b.ContainsPoint(1, 2)) 52 | assert.True(t, b.Equals(b)) //nolint:gocritic 53 | assert.False(t, b.Equals(geos.NewBox2D(1, 2, 3, 4))) 54 | assert.False(t, b.Equals(geos.NewBox2DEmpty())) 55 | expectedWKT := "POINT (0 0)" 56 | if geos.VersionCompare(3, 12, 0) < 0 { 57 | expectedWKT = "POINT (0.0000000000000000 0.0000000000000000)" 58 | } 59 | assert.Equal(t, expectedWKT, b.Geom().ToWKT()) 60 | assert.False(t, b.IsEmpty()) 61 | assert.Equal(t, 0.0, b.Height()) 62 | assert.True(t, b.IsPoint()) 63 | assert.Equal(t, 0.0, b.Width()) 64 | } 65 | -------------------------------------------------------------------------------- /box3d.go: -------------------------------------------------------------------------------- 1 | package geos 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | ) 7 | 8 | // A Box3D is a three-dimensional bounds. 9 | type Box3D struct { 10 | MinX float64 11 | MinY float64 12 | MinZ float64 13 | MaxX float64 14 | MaxY float64 15 | MaxZ float64 16 | } 17 | 18 | // NewBox3D returns a new bounds. 19 | func NewBox3D(minX, minY, minZ, maxX, maxY, maxZ float64) *Box3D { 20 | return &Box3D{ 21 | MinX: minX, 22 | MinY: minY, 23 | MinZ: minZ, 24 | MaxX: maxX, 25 | MaxY: maxY, 26 | MaxZ: maxZ, 27 | } 28 | } 29 | 30 | // NewBox3DEmpty returns a new empty bounds. 31 | func NewBox3DEmpty() *Box3D { 32 | return &Box3D{ 33 | MinX: math.Inf(1), 34 | MinY: math.Inf(1), 35 | MinZ: math.Inf(1), 36 | MaxX: math.Inf(-1), 37 | MaxY: math.Inf(-1), 38 | MaxZ: math.Inf(-1), 39 | } 40 | } 41 | 42 | func (b *Box3D) String() string { 43 | return fmt.Sprintf("[%f %f %f %f %f %f]", b.MinX, b.MinY, b.MinZ, b.MaxX, b.MaxY, b.MaxX) 44 | } 45 | -------------------------------------------------------------------------------- /bufferparams.go: -------------------------------------------------------------------------------- 1 | package geos 2 | 3 | // #include "go-geos.h" 4 | import "C" 5 | 6 | // A BufferParams contains parameters for BufferWithParams. 7 | type BufferParams struct { 8 | context *Context 9 | bufferParams *C.struct_GEOSBufParams_t 10 | } 11 | 12 | // Destroy destroys all resources associated with p. 13 | func (p *BufferParams) Destroy() { 14 | // Protect against Destroy being called more than once. 15 | if p == nil || p.context == nil { 16 | return 17 | } 18 | p.context.Lock() 19 | defer p.context.Unlock() 20 | C.GEOSBufferParams_destroy_r(p.context.handle, p.bufferParams) 21 | *p = BufferParams{} // Clear all references. 22 | } 23 | 24 | // SetEndCapStyle sets p's end cap style. 25 | func (p *BufferParams) SetEndCapStyle(style BufCapStyle) *BufferParams { 26 | p.context.Lock() 27 | defer p.context.Unlock() 28 | if C.GEOSBufferParams_setEndCapStyle_r(p.context.handle, p.bufferParams, C.int(style)) != 1 { 29 | panic(p.context.err) 30 | } 31 | return p 32 | } 33 | 34 | // SetJoinStyle sets p's join style. 35 | func (p *BufferParams) SetJoinStyle(style BufJoinStyle) *BufferParams { 36 | p.context.Lock() 37 | defer p.context.Unlock() 38 | if C.GEOSBufferParams_setJoinStyle_r(p.context.handle, p.bufferParams, C.int(style)) != 1 { 39 | panic(p.context.err) 40 | } 41 | return p 42 | } 43 | 44 | // SetMitreLimit sets p's mitre limit. 45 | func (p *BufferParams) SetMitreLimit(mitreLimit float64) *BufferParams { 46 | p.context.Lock() 47 | defer p.context.Unlock() 48 | if C.GEOSBufferParams_setMitreLimit_r(p.context.handle, p.bufferParams, C.double(mitreLimit)) != 1 { 49 | panic(p.context.err) 50 | } 51 | return p 52 | } 53 | 54 | // SetQuadrantSegments sets the number of segments to stroke each quadrant of 55 | // circular arcs. 56 | func (p *BufferParams) SetQuadrantSegments(quadSegs int) *BufferParams { 57 | p.context.Lock() 58 | defer p.context.Unlock() 59 | if C.GEOSBufferParams_setQuadrantSegments_r(p.context.handle, p.bufferParams, C.int(quadSegs)) != 1 { 60 | panic(p.context.err) 61 | } 62 | return p 63 | } 64 | 65 | // SetSingleSided sets whether the computed buffer should be single sided. 66 | func (p *BufferParams) SetSingleSided(singleSided bool) *BufferParams { 67 | p.context.Lock() 68 | defer p.context.Unlock() 69 | if C.GEOSBufferParams_setSingleSided_r(p.context.handle, p.bufferParams, C.int(intFromBool(singleSided))) != 1 { 70 | panic(p.context.err) 71 | } 72 | return p 73 | } 74 | 75 | func (p *BufferParams) finalize() { 76 | if p.context == nil { 77 | return 78 | } 79 | p.Destroy() 80 | } 81 | 82 | func intFromBool(b bool) int { 83 | if b { 84 | return 1 85 | } 86 | return 0 87 | } 88 | -------------------------------------------------------------------------------- /bufferparams_test.go: -------------------------------------------------------------------------------- 1 | package geos_test 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | 9 | "github.com/twpayne/go-geos" 10 | ) 11 | 12 | func TestBufferWithParams(t *testing.T) { 13 | defer runtime.GC() // Exercise finalizers. 14 | c := geos.NewContext() 15 | p := c.NewBufferParams() 16 | assert.NotZero(t, p) 17 | assert.NotZero(t, p.SetJoinStyle(geos.BufJoinStyleMitre)) 18 | assert.NotZero(t, p.SetEndCapStyle(geos.BufCapStyleSquare)) 19 | assert.NotZero(t, p.SetMitreLimit(1)) 20 | assert.NotZero(t, p.SetQuadrantSegments(1)) 21 | assert.NotZero(t, p.SetSingleSided(true)) 22 | g := c.NewLineString([][]float64{{0, 0}, {1, 0}}).BufferWithParams(p, 1) 23 | assert.NotZero(t, g) 24 | assert.Equal(t, geos.TypeIDPolygon, g.TypeID()) 25 | assert.Equal(t, [][]float64{{1, 0}, {0, 0}, {0, 1}, {1, 1}, {1, 0}}, g.ExteriorRing().CoordSeq().ToCoords()) 26 | } 27 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | package geos 2 | 3 | // #include 4 | // #include "go-geos.h" 5 | import "C" 6 | 7 | import ( 8 | "runtime" 9 | "sync" 10 | "unsafe" 11 | ) 12 | 13 | // A Context is a context. 14 | type Context struct { 15 | sync.Mutex 16 | handle C.GEOSContextHandle_t 17 | ewkbWithSRIDWriter *C.struct_GEOSWKBWriter_t 18 | geoJSONReader *C.struct_GEOSGeoJSONReader_t 19 | geoJSONWriter *C.struct_GEOSGeoJSONWriter_t 20 | wkbReader *C.struct_GEOSWKBReader_t 21 | wkbWriter *C.struct_GEOSWKBWriter_t 22 | wktReader *C.struct_GEOSWKTReader_t 23 | wktWriter *C.struct_GEOSWKTWriter_t 24 | err error 25 | geomFinalizeFunc func(*Geom) 26 | strTreeFinalizeFunc func(*STRtree) 27 | } 28 | 29 | // A ContextOption sets an option on a Context. 30 | type ContextOption func(*Context) 31 | 32 | // WithGeomFinalizeFunc sets a function to be called just before a geometry is 33 | // finalized. This is typically used to log the geometry to help debug geometry 34 | // leaks. 35 | func WithGeomFinalizeFunc(geomFinalizeFunc func(*Geom)) ContextOption { 36 | return func(c *Context) { 37 | c.geomFinalizeFunc = geomFinalizeFunc 38 | } 39 | } 40 | 41 | // WithSTRtreeFinalizeFunc sets a function to be called just before an STRtree 42 | // is finalized. This is typically used to log the STRtree to help debug STRtree 43 | // leaks. 44 | func WithSTRtreeFinalizeFunc(strTreeFinalizeFunc func(*STRtree)) ContextOption { 45 | return func(c *Context) { 46 | c.strTreeFinalizeFunc = strTreeFinalizeFunc 47 | } 48 | } 49 | 50 | // NewContext returns a new Context. 51 | func NewContext(options ...ContextOption) *Context { 52 | c := &Context{ 53 | handle: C.GEOS_init_r(), 54 | } 55 | runtime.SetFinalizer(c, (*Context).finish) 56 | // FIXME in GitHub Actions, golangci-lint complains about the following line saying: 57 | // Error: dupSubExpr: suspicious identical LHS and RHS for `==` operator (gocritic) 58 | // As the line does not contain an `==` operator, disable gocritic on this line. 59 | //nolint:gocritic 60 | C.GEOSContext_setErrorMessageHandler_r(c.handle, C.GEOSMessageHandler_r(C.c_errorMessageHandler), unsafe.Pointer(&c.err)) 61 | for _, option := range options { 62 | option(c) 63 | } 64 | return c 65 | } 66 | 67 | // Clone clones g into c. 68 | func (c *Context) Clone(g *Geom) *Geom { 69 | if g.context == c { 70 | return g.Clone() 71 | } 72 | // FIXME use a more intelligent method than a WKB roundtrip (although a WKB 73 | // roundtrip might actually be quite fast if the cgo overhead is 74 | // significant) 75 | clone, err := c.NewGeomFromWKB(g.ToWKB()) 76 | if err != nil { 77 | panic(err) 78 | } 79 | return clone 80 | } 81 | 82 | // NewBufferParams returns a new BufferParams. 83 | func (c *Context) NewBufferParams() *BufferParams { 84 | c.Lock() 85 | defer c.Unlock() 86 | cBufferParams := C.GEOSBufferParams_create_r(c.handle) 87 | if cBufferParams == nil { 88 | panic(c.err) 89 | } 90 | return c.newBufParams(cBufferParams) 91 | } 92 | 93 | // NewCollection returns a new collection. 94 | func (c *Context) NewCollection(typeID TypeID, geoms []*Geom) *Geom { 95 | if len(geoms) == 0 { 96 | return c.NewEmptyCollection(typeID) 97 | } 98 | c.Lock() 99 | defer c.Unlock() 100 | cGeoms := make([]*C.GEOSGeometry, len(geoms)) 101 | for i, geom := range geoms { 102 | cGeoms[i] = geom.geom 103 | } 104 | g := c.newNonNilGeom(C.GEOSGeom_createCollection_r(c.handle, C.int(typeID), &cGeoms[0], C.uint(len(geoms))), nil) 105 | for _, geom := range geoms { 106 | geom.parent = g 107 | } 108 | return g 109 | } 110 | 111 | // NewCoordSeq returns a new CoordSeq. 112 | func (c *Context) NewCoordSeq(size, dims int) *CoordSeq { 113 | c.Lock() 114 | defer c.Unlock() 115 | return c.newNonNilCoordSeq(C.GEOSCoordSeq_create_r(c.handle, C.uint(size), C.uint(dims))) 116 | } 117 | 118 | // NewCoordSeqFromCoords returns a new CoordSeq populated with coords. 119 | func (c *Context) NewCoordSeqFromCoords(coords [][]float64) *CoordSeq { 120 | c.Lock() 121 | defer c.Unlock() 122 | return c.newNonNilCoordSeq(c.newGEOSCoordSeqFromCoords(coords)) 123 | } 124 | 125 | // NewEmptyCollection returns a new empty collection. 126 | func (c *Context) NewEmptyCollection(typeID TypeID) *Geom { 127 | c.Lock() 128 | defer c.Unlock() 129 | return c.newNonNilGeom(C.GEOSGeom_createEmptyCollection_r(c.handle, C.int(typeID)), nil) 130 | } 131 | 132 | // NewEmptyLineString returns a new empty line string. 133 | func (c *Context) NewEmptyLineString() *Geom { 134 | c.Lock() 135 | defer c.Unlock() 136 | return c.newNonNilGeom(C.GEOSGeom_createEmptyLineString_r(c.handle), nil) 137 | } 138 | 139 | // NewEmptyPoint returns a new empty point. 140 | func (c *Context) NewEmptyPoint() *Geom { 141 | c.Lock() 142 | defer c.Unlock() 143 | return c.newNonNilGeom(C.GEOSGeom_createEmptyPoint_r(c.handle), nil) 144 | } 145 | 146 | // NewEmptyPolygon returns a new empty polygon. 147 | func (c *Context) NewEmptyPolygon() *Geom { 148 | c.Lock() 149 | defer c.Unlock() 150 | return c.newNonNilGeom(C.GEOSGeom_createEmptyPolygon_r(c.handle), nil) 151 | } 152 | 153 | // NewGeomFromBounds returns a new polygon constructed from bounds. 154 | func (c *Context) NewGeomFromBounds(minX, minY, maxX, maxY float64) *Geom { 155 | var typeID C.int 156 | geom := C.c_newGEOSGeomFromBounds_r(c.handle, &typeID, C.double(minX), C.double(minY), C.double(maxX), C.double(maxY)) 157 | if geom == nil { 158 | panic(c.err) 159 | } 160 | g := &Geom{ 161 | context: c, 162 | geom: geom, 163 | typeID: TypeID(typeID), 164 | numGeometries: 1, 165 | } 166 | runtime.SetFinalizer(g, (*Geom).finalize) 167 | return g 168 | } 169 | 170 | // NewGeomFromGeoJSON returns a new geometry in JSON format from json. 171 | func (c *Context) NewGeomFromGeoJSON(geoJSON string) (*Geom, error) { 172 | c.Lock() 173 | defer c.Unlock() 174 | c.err = nil 175 | if c.geoJSONReader == nil { 176 | c.geoJSONReader = C.GEOSGeoJSONReader_create_r(c.handle) 177 | } 178 | geoJSONCStr := C.CString(geoJSON) 179 | defer C.free(unsafe.Pointer(geoJSONCStr)) 180 | return c.newGeom(C.GEOSGeoJSONReader_readGeometry_r(c.handle, c.geoJSONReader, geoJSONCStr), nil), c.err 181 | } 182 | 183 | // NewGeomFromWKB parses a geometry in WKB format from wkb. 184 | func (c *Context) NewGeomFromWKB(wkb []byte) (*Geom, error) { 185 | c.Lock() 186 | defer c.Unlock() 187 | c.err = nil 188 | if c.wkbReader == nil { 189 | c.wkbReader = C.GEOSWKBReader_create_r(c.handle) 190 | } 191 | wkbCBuf := C.CBytes(wkb) 192 | defer C.free(wkbCBuf) 193 | return c.newGeom(C.GEOSWKBReader_read_r(c.handle, c.wkbReader, (*C.uchar)(wkbCBuf), C.ulong(len(wkb))), nil), c.err 194 | } 195 | 196 | // NewGeomFromWKT parses a geometry in WKT format from wkt. 197 | func (c *Context) NewGeomFromWKT(wkt string) (*Geom, error) { 198 | c.Lock() 199 | defer c.Unlock() 200 | c.err = nil 201 | if c.wktReader == nil { 202 | c.wktReader = C.GEOSWKTReader_create_r(c.handle) 203 | } 204 | wktCStr := C.CString(wkt) 205 | defer C.free(unsafe.Pointer(wktCStr)) 206 | return c.newGeom(C.GEOSWKTReader_read_r(c.handle, c.wktReader, wktCStr), nil), c.err 207 | } 208 | 209 | // NewLinearRing returns a new linear ring populated with coords. 210 | func (c *Context) NewLinearRing(coords [][]float64) *Geom { 211 | c.Lock() 212 | defer c.Unlock() 213 | s := c.newGEOSCoordSeqFromCoords(coords) 214 | return c.newNonNilGeom(C.GEOSGeom_createLinearRing_r(c.handle, s), nil) 215 | } 216 | 217 | // NewLineString returns a new line string populated with coords. 218 | func (c *Context) NewLineString(coords [][]float64) *Geom { 219 | c.Lock() 220 | defer c.Unlock() 221 | s := c.newGEOSCoordSeqFromCoords(coords) 222 | return c.newNonNilGeom(C.GEOSGeom_createLineString_r(c.handle, s), nil) 223 | } 224 | 225 | // NewPoint returns a new point populated with coord. 226 | func (c *Context) NewPoint(coord []float64) *Geom { 227 | s := c.newGEOSCoordSeqFromCoords([][]float64{coord}) 228 | return c.newNonNilGeom(C.GEOSGeom_createPoint_r(c.handle, s), nil) 229 | } 230 | 231 | // NewPointFromXY returns a new point with a x and y. 232 | func (c *Context) NewPointFromXY(x, y float64) *Geom { 233 | return c.newNonNilGeom(C.GEOSGeom_createPointFromXY_r(c.handle, C.double(x), C.double(y)), nil) 234 | } 235 | 236 | // NewPoints returns a new slice of points populated from coords. 237 | func (c *Context) NewPoints(coords [][]float64) []*Geom { 238 | if coords == nil { 239 | return nil 240 | } 241 | geoms := make([]*Geom, len(coords)) 242 | for i := range geoms { 243 | geoms[i] = c.NewPoint(coords[i]) 244 | } 245 | return geoms 246 | } 247 | 248 | // NewPolygon returns a new point populated with coordss. 249 | func (c *Context) NewPolygon(coordss [][][]float64) *Geom { 250 | if len(coordss) == 0 { 251 | return c.NewEmptyPolygon() 252 | } 253 | var ( 254 | shell *C.struct_GEOSGeom_t 255 | holesSlice []*C.struct_GEOSGeom_t 256 | ) 257 | defer func() { 258 | if v := recover(); v != nil { 259 | C.GEOSGeom_destroy_r(c.handle, shell) 260 | for _, hole := range holesSlice { 261 | C.GEOSGeom_destroy_r(c.handle, hole) 262 | } 263 | panic(v) 264 | } 265 | }() 266 | shell = C.GEOSGeom_createLinearRing_r(c.handle, c.newGEOSCoordSeqFromCoords(coordss[0])) 267 | if shell == nil { 268 | panic(c.err) 269 | } 270 | var holes **C.struct_GEOSGeom_t 271 | nholes := len(coordss) - 1 272 | if nholes > 0 { 273 | holesSlice = make([]*C.struct_GEOSGeom_t, nholes) 274 | for i := range holesSlice { 275 | hole := C.GEOSGeom_createLinearRing_r(c.handle, c.newGEOSCoordSeqFromCoords(coordss[i+1])) 276 | if hole == nil { 277 | panic(c.err) 278 | } 279 | holesSlice[i] = hole 280 | } 281 | holes = (**C.struct_GEOSGeom_t)(unsafe.Pointer(&holesSlice[0])) 282 | } 283 | return c.newNonNilGeom(C.GEOSGeom_createPolygon_r(c.handle, shell, holes, C.uint(nholes)), nil) 284 | } 285 | 286 | // NewSTRtree returns a new STRtree. 287 | func (c *Context) NewSTRtree(nodeCapacity int) *STRtree { 288 | c.Lock() 289 | defer c.Unlock() 290 | t := &STRtree{ 291 | context: c, 292 | strTree: C.GEOSSTRtree_create_r(c.handle, C.size_t(nodeCapacity)), 293 | itemToValue: make(map[unsafe.Pointer]any), 294 | valueToItem: make(map[any]unsafe.Pointer), 295 | } 296 | runtime.SetFinalizer(t, (*STRtree).finalize) 297 | return t 298 | } 299 | 300 | // OrientationIndex returns the orientation index from A to B and then to P. 301 | func (c *Context) OrientationIndex(ax, ay, bx, by, px, py float64) int { 302 | c.Lock() 303 | defer c.Unlock() 304 | return int(C.GEOSOrientationIndex_r(c.handle, C.double(ax), C.double(ay), C.double(bx), C.double(by), C.double(px), C.double(py))) 305 | } 306 | 307 | // Polygonize returns a set of geometries which contains linework that 308 | // represents the edges of a planar graph. 309 | func (c *Context) Polygonize(geoms []*Geom) *Geom { 310 | c.Lock() 311 | defer c.Unlock() 312 | cGeoms, unlockFunc := c.cGeomsLocked(geoms) 313 | defer unlockFunc() 314 | return c.newNonNilGeom(C.GEOSPolygonize_r(c.handle, cGeoms, C.uint(len(geoms))), nil) 315 | } 316 | 317 | // PolygonizeValid returns a set of polygons which contains linework that 318 | // represents the edges of a planar graph. 319 | func (c *Context) PolygonizeValid(geoms []*Geom) *Geom { 320 | c.Lock() 321 | defer c.Unlock() 322 | cGeoms, unlockFunc := c.cGeomsLocked(geoms) 323 | defer unlockFunc() 324 | return c.newNonNilGeom(C.GEOSPolygonize_valid_r(c.handle, cGeoms, C.uint(len(geoms))), nil) 325 | } 326 | 327 | // RelatePatternMatch returns if two DE9IM patterns are consistent. 328 | func (c *Context) RelatePatternMatch(mat, pat string) bool { 329 | matCStr := C.CString(mat) 330 | defer C.free(unsafe.Pointer(matCStr)) 331 | patCStr := C.CString(pat) 332 | defer C.free(unsafe.Pointer(patCStr)) 333 | c.Lock() 334 | defer c.Unlock() 335 | switch C.GEOSRelatePatternMatch_r(c.handle, matCStr, patCStr) { 336 | case 0: 337 | return false 338 | case 1: 339 | return true 340 | default: 341 | panic(c.err) 342 | } 343 | } 344 | 345 | // SegmentIntersection returns the coordinate where two lines intersect. 346 | func (c *Context) SegmentIntersection(ax0, ay0, ax1, ay1, bx0, by0, bx1, by1 float64) (x, y float64, intersection bool) { 347 | c.Lock() 348 | defer c.Unlock() 349 | var cx, cy float64 350 | switch C.GEOSSegmentIntersection_r(c.handle, 351 | C.double(ax0), C.double(ay0), C.double(ax1), C.double(ay1), 352 | C.double(bx0), C.double(by0), C.double(bx1), C.double(by1), 353 | (*C.double)(&cx), (*C.double)(&cy)) { 354 | case 1: 355 | return cx, cy, true 356 | case -1: 357 | return 0, 0, false 358 | default: 359 | panic(c.err) 360 | } 361 | } 362 | 363 | func (c *Context) cGeomsLocked(geoms []*Geom) (**C.struct_GEOSGeom_t, func()) { 364 | if len(geoms) == 0 { 365 | return nil, func() {} 366 | } 367 | uniqueContexts := map[*Context]struct{}{c: {}} 368 | var extraContexts []*Context 369 | cGeoms := make([]*C.struct_GEOSGeom_t, len(geoms)) 370 | for i := range cGeoms { 371 | geom := geoms[i] 372 | geom.mustNotBeDestroyed() 373 | if _, ok := uniqueContexts[geom.context]; !ok { 374 | geom.context.Lock() 375 | uniqueContexts[geom.context] = struct{}{} 376 | extraContexts = append(extraContexts, geom.context) 377 | } 378 | cGeoms[i] = geom.geom 379 | } 380 | return &cGeoms[0], func() { 381 | for i := len(extraContexts) - 1; i >= 0; i-- { 382 | extraContexts[i].Unlock() 383 | } 384 | } 385 | } 386 | 387 | func (c *Context) finish() { 388 | c.Lock() 389 | defer c.Unlock() 390 | if c.ewkbWithSRIDWriter != nil { 391 | C.GEOSWKBWriter_destroy_r(c.handle, c.ewkbWithSRIDWriter) 392 | } 393 | if c.geoJSONReader != nil { 394 | C.GEOSGeoJSONReader_destroy_r(c.handle, c.geoJSONReader) 395 | } 396 | if c.geoJSONWriter != nil { 397 | C.GEOSGeoJSONWriter_destroy_r(c.handle, c.geoJSONWriter) 398 | } 399 | if c.wkbReader != nil { 400 | C.GEOSWKBReader_destroy_r(c.handle, c.wkbReader) 401 | } 402 | if c.wkbWriter != nil { 403 | C.GEOSWKBWriter_destroy_r(c.handle, c.wkbWriter) 404 | } 405 | if c.wktReader != nil { 406 | C.GEOSWKTReader_destroy_r(c.handle, c.wktReader) 407 | } 408 | if c.wktWriter != nil { 409 | C.GEOSWKTWriter_destroy_r(c.handle, c.wktWriter) 410 | } 411 | C.finishGEOS_r(c.handle) 412 | } 413 | 414 | func (c *Context) newBufParams(p *C.struct_GEOSBufParams_t) *BufferParams { 415 | if p == nil { 416 | return nil 417 | } 418 | bufParams := &BufferParams{ 419 | context: c, 420 | bufferParams: p, 421 | } 422 | runtime.SetFinalizer(bufParams, (*BufferParams).finalize) 423 | return bufParams 424 | } 425 | 426 | func (c *Context) newCoordSeqInternal(gs *C.struct_GEOSCoordSeq_t, finalizer func(*CoordSeq)) *CoordSeq { 427 | if gs == nil { 428 | return nil 429 | } 430 | var ( 431 | dimensions C.uint 432 | size C.uint 433 | ) 434 | if C.GEOSCoordSeq_getDimensions_r(c.handle, gs, &dimensions) == 0 { 435 | panic(c.err) 436 | } 437 | if C.GEOSCoordSeq_getSize_r(c.handle, gs, &size) == 0 { 438 | panic(c.err) 439 | } 440 | s := &CoordSeq{ 441 | context: c, 442 | s: gs, 443 | dimensions: int(dimensions), 444 | size: int(size), 445 | } 446 | if finalizer != nil { 447 | runtime.SetFinalizer(s, finalizer) 448 | } 449 | return s 450 | } 451 | 452 | func (c *Context) newCoordsFromGEOSCoordSeq(s *C.struct_GEOSCoordSeq_t) [][]float64 { 453 | var dimensions C.uint 454 | if C.GEOSCoordSeq_getDimensions_r(c.handle, s, &dimensions) == 0 { 455 | panic(c.err) 456 | } 457 | 458 | var size C.uint 459 | if C.GEOSCoordSeq_getSize_r(c.handle, s, &size) == 0 { 460 | panic(c.err) 461 | } 462 | 463 | var hasZ C.int 464 | if dimensions > 2 { 465 | hasZ = 1 466 | } 467 | 468 | var hasM C.int 469 | if dimensions > 3 { 470 | hasM = 1 471 | } 472 | 473 | flatCoords := make([]float64, size*dimensions) 474 | if C.GEOSCoordSeq_copyToBuffer_r(c.handle, s, (*C.double)(&flatCoords[0]), hasZ, hasM) == 0 { 475 | panic(c.err) 476 | } 477 | coords := make([][]float64, size) 478 | for i := range coords { 479 | coord := flatCoords[i*int(dimensions) : (i+1)*int(dimensions) : (i+1)*int(dimensions)] 480 | coords[i] = coord 481 | } 482 | return coords 483 | } 484 | 485 | func (c *Context) newGEOSCoordSeqFromCoords(coords [][]float64) *C.struct_GEOSCoordSeq_t { 486 | var hasZ C.int 487 | if len(coords[0]) > 2 { 488 | hasZ = 1 489 | } 490 | 491 | var hasM C.int 492 | if len(coords[0]) > 3 { 493 | hasM = 1 494 | } 495 | 496 | dimensions := len(coords[0]) 497 | flatCoords := make([]float64, len(coords)*dimensions) 498 | for i, coord := range coords { 499 | copy(flatCoords[i*dimensions:(i+1)*dimensions], coord) 500 | } 501 | return C.GEOSCoordSeq_copyFromBuffer_r(c.handle, (*C.double)(unsafe.Pointer(&flatCoords[0])), C.uint(len(coords)), hasZ, hasM) 502 | } 503 | 504 | func (c *Context) newGeom(geom *C.struct_GEOSGeom_t, parent *Geom) *Geom { 505 | if geom == nil { 506 | return nil 507 | } 508 | var ( 509 | typeID C.int 510 | numGeometries C.int 511 | numPoints C.int 512 | numInteriorRings C.int 513 | ) 514 | if C.c_GEOSGeomGetInfo_r(c.handle, geom, &typeID, &numGeometries, &numPoints, &numInteriorRings) == 0 { 515 | panic(c.err) 516 | } 517 | g := &Geom{ 518 | context: c, 519 | geom: geom, 520 | parent: parent, 521 | typeID: TypeID(typeID), 522 | numGeometries: int(numGeometries), 523 | numInteriorRings: int(numInteriorRings), 524 | numPoints: int(numPoints), 525 | } 526 | runtime.SetFinalizer(g, (*Geom).finalize) 527 | return g 528 | } 529 | 530 | func (c *Context) newNonNilBufferParams(p *C.struct_GEOSBufParams_t) *BufferParams { 531 | if p == nil { 532 | panic(c.err) 533 | } 534 | return c.newBufParams(p) 535 | } 536 | 537 | func (c *Context) newNonNilCoordSeq(s *C.struct_GEOSCoordSeq_t) *CoordSeq { 538 | if s == nil { 539 | panic(c.err) 540 | } 541 | return c.newCoordSeqInternal(s, (*CoordSeq).Destroy) 542 | } 543 | 544 | func (c *Context) newNonNilGeom(geom *C.struct_GEOSGeom_t, parent *Geom) *Geom { 545 | if geom == nil { 546 | panic(c.err) 547 | } 548 | return c.newGeom(geom, parent) 549 | } 550 | 551 | //export go_errorMessageHandler 552 | func go_errorMessageHandler(message *C.char, userdata unsafe.Pointer) { 553 | errP := (*error)(userdata) 554 | *errP = Error(C.GoString(message)) 555 | } 556 | -------------------------------------------------------------------------------- /context_test.go: -------------------------------------------------------------------------------- 1 | package geos_test 2 | 3 | import ( 4 | "math" 5 | "runtime" 6 | "strconv" 7 | "sync" 8 | "testing" 9 | 10 | "github.com/alecthomas/assert/v2" 11 | 12 | "github.com/twpayne/go-geos" 13 | ) 14 | 15 | func TestGeometryConstructors(t *testing.T) { 16 | for _, tc := range []struct { 17 | name string 18 | newGeomFunc func(*geos.Context) *geos.Geom 19 | expectedWKT string 20 | }{ 21 | { 22 | name: "NewCollection_MultiPoint_empty", 23 | newGeomFunc: func(c *geos.Context) *geos.Geom { 24 | return c.NewCollection(geos.TypeIDMultiPoint, nil) 25 | }, 26 | expectedWKT: "MULTIPOINT EMPTY", 27 | }, 28 | { 29 | name: "NewCollection_MultiPoint_one", 30 | newGeomFunc: func(c *geos.Context) *geos.Geom { 31 | return c.NewCollection(geos.TypeIDMultiPoint, []*geos.Geom{ 32 | c.NewPoint([]float64{0, 1}), 33 | }) 34 | }, 35 | expectedWKT: "MULTIPOINT ((0 1))", 36 | }, 37 | { 38 | name: "NewCollection_MultiPoint_many", 39 | newGeomFunc: func(c *geos.Context) *geos.Geom { 40 | return c.NewCollection(geos.TypeIDMultiPoint, []*geos.Geom{ 41 | c.NewPoint([]float64{0, 1}), 42 | c.NewPoint([]float64{2, 3}), 43 | c.NewPoint([]float64{4, 5}), 44 | }) 45 | }, 46 | expectedWKT: "MULTIPOINT ((0 1), (2 3), (4 5))", 47 | }, 48 | { 49 | name: "NewCollection_MultiLineString_empty", 50 | newGeomFunc: func(c *geos.Context) *geos.Geom { 51 | return c.NewCollection(geos.TypeIDMultiLineString, nil) 52 | }, 53 | expectedWKT: "MULTILINESTRING EMPTY", 54 | }, 55 | { 56 | name: "NewCollection_MultiPolygon_empty", 57 | newGeomFunc: func(c *geos.Context) *geos.Geom { 58 | return c.NewCollection(geos.TypeIDMultiPolygon, nil) 59 | }, 60 | expectedWKT: "MULTIPOLYGON EMPTY", 61 | }, 62 | { 63 | name: "NewCollection_GeometryCollection_empty", 64 | newGeomFunc: func(c *geos.Context) *geos.Geom { 65 | return c.NewCollection(geos.TypeIDGeometryCollection, nil) 66 | }, 67 | expectedWKT: "GEOMETRYCOLLECTION EMPTY", 68 | }, 69 | { 70 | name: "NewCollection_GeometryCollection_many", 71 | newGeomFunc: func(c *geos.Context) *geos.Geom { 72 | return c.NewCollection(geos.TypeIDGeometryCollection, []*geos.Geom{ 73 | c.NewPoint([]float64{0, 1}), 74 | c.NewLineString([][]float64{{2, 3}, {4, 5}}), 75 | c.NewCollection(geos.TypeIDMultiPoint, []*geos.Geom{ 76 | c.NewPoint([]float64{6, 7}), 77 | }), 78 | }) 79 | }, 80 | expectedWKT: "GEOMETRYCOLLECTION (POINT (0 1), LINESTRING (2 3, 4 5), MULTIPOINT (6 7))", 81 | }, 82 | { 83 | name: "NewEmptyCollection_MultiPoint", 84 | newGeomFunc: func(c *geos.Context) *geos.Geom { 85 | return c.NewEmptyCollection(geos.TypeIDMultiPoint) 86 | }, 87 | expectedWKT: "MULTIPOINT EMPTY", 88 | }, 89 | { 90 | name: "NewEmptyCollection_MultiLineString", 91 | newGeomFunc: func(c *geos.Context) *geos.Geom { 92 | return c.NewEmptyCollection(geos.TypeIDMultiLineString) 93 | }, 94 | expectedWKT: "MULTILINESTRING EMPTY", 95 | }, 96 | { 97 | name: "NewEmptyCollection_MultiPolygon", 98 | newGeomFunc: func(c *geos.Context) *geos.Geom { 99 | return c.NewEmptyCollection(geos.TypeIDMultiPolygon) 100 | }, 101 | expectedWKT: "MULTIPOLYGON EMPTY", 102 | }, 103 | { 104 | name: "NewEmptyCollection_GeometryCollection", 105 | newGeomFunc: func(c *geos.Context) *geos.Geom { 106 | return c.NewEmptyCollection(geos.TypeIDGeometryCollection) 107 | }, 108 | expectedWKT: "GEOMETRYCOLLECTION EMPTY", 109 | }, 110 | { 111 | name: "NewEmptyPoint", 112 | newGeomFunc: func(c *geos.Context) *geos.Geom { 113 | return c.NewEmptyPoint() 114 | }, 115 | expectedWKT: "POINT EMPTY", 116 | }, 117 | { 118 | name: "NewGeomFromBounds_polygon", 119 | newGeomFunc: func(c *geos.Context) *geos.Geom { 120 | return c.NewGeomFromBounds(0, 1, 2, 3) 121 | }, 122 | expectedWKT: "POLYGON ((0 1, 2 1, 2 3, 0 3, 0 1))", 123 | }, 124 | { 125 | name: "NewGeomFromBounds_empty", 126 | newGeomFunc: func(c *geos.Context) *geos.Geom { 127 | return c.NewGeomFromBounds(math.Inf(1), math.Inf(1), math.Inf(-1), math.Inf(-1)) 128 | }, 129 | expectedWKT: "POINT EMPTY", 130 | }, 131 | { 132 | name: "NewGeomFromBounds_point", 133 | newGeomFunc: func(c *geos.Context) *geos.Geom { 134 | return c.NewGeomFromBounds(0, 1, 0, 1) 135 | }, 136 | expectedWKT: "POINT (0 1)", 137 | }, 138 | { 139 | name: "NewPoint", 140 | newGeomFunc: func(c *geos.Context) *geos.Geom { 141 | return c.NewPoint([]float64{1, 2}) 142 | }, 143 | expectedWKT: "POINT (1 2)", 144 | }, 145 | { 146 | name: "NewLinearRing", 147 | newGeomFunc: func(c *geos.Context) *geos.Geom { 148 | return c.NewLinearRing([][]float64{{1, 2}, {3, 4}, {5, 6}, {1, 2}}) 149 | }, 150 | expectedWKT: "LINEARRING (1 2, 3 4, 5 6, 1 2)", 151 | }, 152 | { 153 | name: "NewEmptyLineString", 154 | newGeomFunc: func(c *geos.Context) *geos.Geom { 155 | return c.NewEmptyLineString() 156 | }, 157 | expectedWKT: "LINESTRING EMPTY", 158 | }, 159 | { 160 | name: "NewLineString", 161 | newGeomFunc: func(c *geos.Context) *geos.Geom { 162 | return c.NewLineString([][]float64{{1, 2}, {3, 4}}) 163 | }, 164 | expectedWKT: "LINESTRING (1 2, 3 4)", 165 | }, 166 | { 167 | name: "NewEmptyPolygon", 168 | newGeomFunc: func(c *geos.Context) *geos.Geom { 169 | return c.NewEmptyPolygon() 170 | }, 171 | expectedWKT: "POLYGON EMPTY", 172 | }, 173 | { 174 | name: "NewPolygon_empty", 175 | newGeomFunc: func(c *geos.Context) *geos.Geom { 176 | return c.NewPolygon(nil) 177 | }, 178 | expectedWKT: "POLYGON EMPTY", 179 | }, 180 | { 181 | name: "NewPolygon", 182 | newGeomFunc: func(c *geos.Context) *geos.Geom { 183 | return c.NewPolygon([][][]float64{{{0, 0}, {1, 1}, {0, 1}, {0, 0}}}) 184 | }, 185 | expectedWKT: "POLYGON ((0 0, 1 1, 0 1, 0 0))", 186 | }, 187 | { 188 | name: "NewPolygon_with_hole", 189 | newGeomFunc: func(c *geos.Context) *geos.Geom { 190 | return c.NewPolygon([][][]float64{ 191 | {{0, 0}, {3, 0}, {3, 3}, {0, 3}, {0, 0}}, 192 | {{1, 1}, {1, 2}, {2, 2}, {2, 1}, {1, 1}}, 193 | }) 194 | }, 195 | expectedWKT: "POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))", 196 | }, 197 | } { 198 | t.Run(tc.name, func(t *testing.T) { 199 | defer runtime.GC() // Exercise finalizers. 200 | c := geos.NewContext() 201 | g := tc.newGeomFunc(c) 202 | assert.NotZero(t, g) 203 | expectedGeom, err := c.NewGeomFromWKT(tc.expectedWKT) 204 | assert.NoError(t, err) 205 | assert.Equal(t, expectedGeom.TypeID(), g.TypeID()) 206 | assert.True(t, g.Equals(expectedGeom)) 207 | }) 208 | } 209 | } 210 | 211 | func TestFinalizeFunc(t *testing.T) { 212 | var wg sync.WaitGroup 213 | finalizeHookCalled := false 214 | wg.Add(1) 215 | c := geos.NewContext(geos.WithGeomFinalizeFunc(func(_ *geos.Geom) { 216 | defer wg.Done() 217 | finalizeHookCalled = true 218 | })) 219 | _ = c.NewPoint([]float64{0, 0}) 220 | runtime.GC() 221 | wg.Wait() 222 | assert.True(t, finalizeHookCalled) 223 | } 224 | 225 | func TestMultipleContexts(t *testing.T) { 226 | c1, c2 := geos.NewContext(), geos.NewContext() 227 | g1s, g2s := []*geos.Geom{}, []*geos.Geom{} 228 | for _, wkt := range []string{ 229 | "POINT (0 0)", 230 | "LINESTRING (0 0, 0 1)", 231 | "POLYGON ((0 0, 1 0, 1 1, 0 0))", 232 | } { 233 | g1 := mustNewGeomFromWKT(t, c1, wkt) 234 | g1s = append(g1s, g1) 235 | g2 := mustNewGeomFromWKT(t, c2, wkt) 236 | g2s = append(g2s, g2) 237 | } 238 | for _, g1 := range g1s { 239 | for _, g2 := range g2s { 240 | assert.Equal(t, g1.Contains(g2), g2.Contains(g1)) 241 | assert.Equal(t, g1.Equals(g2), g2.Equals(g1)) 242 | assert.Equal(t, g1.Intersects(g2), g2.Intersects(g1)) 243 | g2CloneInC1 := c1.Clone(g2) 244 | assert.Equal(t, g1.Contains(g2CloneInC1), g2CloneInC1.Contains(g1)) 245 | assert.Equal(t, g1.Equals(g2CloneInC1), g2CloneInC1.Equals(g1)) 246 | assert.Equal(t, g1.Intersects(g2CloneInC1), g2CloneInC1.Intersects(g1)) 247 | } 248 | } 249 | } 250 | 251 | func TestNewPoints(t *testing.T) { 252 | c := geos.NewContext() 253 | assert.Equal(t, nil, c.NewPoints(nil)) 254 | gs := c.NewPoints([][]float64{{1, 2}, {3, 4}}) 255 | assert.Equal(t, 2, len(gs)) 256 | assert.True(t, gs[0].Equals(mustNewGeomFromWKT(t, c, "POINT (1 2)"))) 257 | assert.True(t, gs[1].Equals(mustNewGeomFromWKT(t, c, "POINT (3 4)"))) 258 | } 259 | 260 | func TestPolygonize(t *testing.T) { 261 | for _, tc := range []struct { 262 | name string 263 | geomWKTs []string 264 | expectedWKT string 265 | expectedValidWKT string 266 | }{ 267 | { 268 | name: "empty", 269 | expectedWKT: "GEOMETRYCOLLECTION EMPTY", 270 | expectedValidWKT: "GEOMETRYCOLLECTION EMPTY", 271 | }, 272 | { 273 | name: "simple", 274 | geomWKTs: []string{ 275 | "LINESTRING (0 0,1 0,1 1)", 276 | "LINESTRING (1 1,0 1,0 0)", 277 | }, 278 | expectedWKT: "GEOMETRYCOLLECTION (POLYGON ((0 0,1 0,1 1,0 1,0 0)))", 279 | expectedValidWKT: "POLYGON ((0 0,1 0,1 1,0 1,0 0))", 280 | }, 281 | { 282 | name: "extra_linestring", 283 | geomWKTs: []string{ 284 | "LINESTRING (0 0,1 0,1 1)", 285 | "LINESTRING (1 1,0 1,0 0)", 286 | "LINESTRING (0 0,0 -1)", 287 | }, 288 | expectedWKT: "GEOMETRYCOLLECTION (POLYGON ((0 0,1 0,1 1,0 1,0 0)))", 289 | expectedValidWKT: "POLYGON ((0 0,1 0,1 1,0 1,0 0))", 290 | }, 291 | { 292 | name: "two_polygons", 293 | geomWKTs: []string{ 294 | "LINESTRING (0 0,1 0,1 1)", 295 | "LINESTRING (1 1,0 1,0 0)", 296 | "LINESTRING (2 2,3 2,3 3)", 297 | "LINESTRING (3 3,2 3,2 2)", 298 | }, 299 | expectedWKT: "GEOMETRYCOLLECTION (POLYGON ((0 0,1 0,1 1,0 1,0 0)),POLYGON ((2 2,3 2,3 3,2 3,2 2)))", 300 | expectedValidWKT: "MULTIPOLYGON (((0 0,1 0,1 1,0 1,0 0)),((2 2,3 2,3 3,2 3,2 2)))", 301 | }, 302 | } { 303 | t.Run(tc.name, func(t *testing.T) { 304 | c := geos.NewContext() 305 | geoms := make([]*geos.Geom, 0, len(tc.geomWKTs)) 306 | for _, geomWKT := range tc.geomWKTs { 307 | geom := mustNewGeomFromWKT(t, c, geomWKT) 308 | geoms = append(geoms, geom) 309 | } 310 | assert.Equal(t, mustNewGeomFromWKT(t, c, tc.expectedWKT), c.Polygonize(geoms)) 311 | assert.Equal(t, mustNewGeomFromWKT(t, c, tc.expectedValidWKT), c.PolygonizeValid(geoms)) 312 | }) 313 | } 314 | } 315 | 316 | func TestPolygonizeMultiContext(t *testing.T) { 317 | c1 := geos.NewContext() 318 | c2 := geos.NewContext() 319 | for range 4 { 320 | assert.Equal(t, 321 | mustNewGeomFromWKT(t, c1, "GEOMETRYCOLLECTION (POLYGON ((0 0,1 0,1 1,0 1,0 0)))"), 322 | c1.Polygonize([]*geos.Geom{ 323 | mustNewGeomFromWKT(t, c1, "LINESTRING (0 0,1 0)"), 324 | mustNewGeomFromWKT(t, c2, "LINESTRING (1 0,1 1)"), 325 | mustNewGeomFromWKT(t, c1, "LINESTRING (1 1,0 1)"), 326 | mustNewGeomFromWKT(t, c2, "LINESTRING (0 1,0 0)"), 327 | }), 328 | ) 329 | } 330 | } 331 | 332 | func TestSegmentIntersection(t *testing.T) { 333 | for i, tc := range []struct { 334 | ax0, ay0, ax1, ay1 float64 335 | bx0, by0, bx1, by1 float64 336 | cx, cy float64 337 | intersects bool 338 | }{ 339 | { 340 | ax0: 0, 341 | ay0: 0, 342 | ax1: 1, 343 | ay1: 1, 344 | bx0: 0, 345 | by0: 1, 346 | bx1: 1, 347 | by1: 0, 348 | cx: 0.5, 349 | cy: 0.5, 350 | intersects: true, 351 | }, 352 | { 353 | ax0: 0, 354 | ay0: 0, 355 | ax1: 1, 356 | ay1: 0, 357 | bx0: 0, 358 | by0: 1, 359 | bx1: 1, 360 | by1: 1, 361 | }, 362 | { 363 | ax0: 0, 364 | ay0: 0, 365 | ax1: 1, 366 | ay1: 0, 367 | bx0: 0, 368 | by0: 0, 369 | bx1: 1, 370 | by1: 0, 371 | intersects: true, 372 | }, 373 | } { 374 | t.Run(strconv.Itoa(i), func(t *testing.T) { 375 | actualCX, actualCY, actualIntersects := geos.NewContext().SegmentIntersection(tc.ax0, tc.ay0, tc.ax1, tc.ay1, tc.bx0, tc.by0, tc.ax1, tc.by1) 376 | assert.Equal(t, tc.cx, actualCX) 377 | assert.Equal(t, tc.cy, actualCY) 378 | assert.Equal(t, tc.intersects, actualIntersects) 379 | }) 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /coordseq.go: -------------------------------------------------------------------------------- 1 | package geos 2 | 3 | // #include "go-geos.h" 4 | import "C" 5 | 6 | // A CoordSeq is a coordinate sequence. 7 | type CoordSeq struct { 8 | context *Context 9 | s *C.struct_GEOSCoordSeq_t 10 | parent *Geom 11 | dimensions int 12 | size int 13 | } 14 | 15 | // Clone returns a clone of s. 16 | func (s *CoordSeq) Clone() *CoordSeq { 17 | s.context.Lock() 18 | defer s.context.Unlock() 19 | return s.context.newNonNilCoordSeq(C.GEOSCoordSeq_clone_r(s.context.handle, s.s)) 20 | } 21 | 22 | // Destroy destroys s and all resources associated with s. 23 | func (s *CoordSeq) Destroy() { 24 | if s == nil || s.context == nil { 25 | return 26 | } 27 | s.context.Lock() 28 | defer s.context.Unlock() 29 | C.GEOSCoordSeq_destroy_r(s.context.handle, s.s) 30 | *s = CoordSeq{} // Clear all references. 31 | } 32 | 33 | // Dimensions returns the dimensions of s. 34 | func (s *CoordSeq) Dimensions() int { 35 | return s.dimensions 36 | } 37 | 38 | // IsCCW returns if s is counter-clockwise. 39 | func (s *CoordSeq) IsCCW() bool { 40 | s.context.Lock() 41 | defer s.context.Unlock() 42 | var cIsCCW C.char 43 | switch C.GEOSCoordSeq_isCCW_r(s.context.handle, s.s, &cIsCCW) { 44 | case 1: 45 | return cIsCCW != 0 46 | default: 47 | panic(s.context.err) 48 | } 49 | } 50 | 51 | // Ordinate returns the idx-th dim coordinate of s. 52 | func (s *CoordSeq) Ordinate(idx, dim int) float64 { 53 | s.context.Lock() 54 | defer s.context.Unlock() 55 | if idx < 0 || s.size <= idx { 56 | panic(errIndexOutOfRange) 57 | } 58 | if dim < 0 || s.dimensions <= dim { 59 | panic(errDimensionOutOfRange) 60 | } 61 | var value float64 62 | if C.GEOSCoordSeq_getOrdinate_r(s.context.handle, s.s, C.uint(idx), C.uint(dim), (*C.double)(&value)) == 0 { 63 | panic(s.context.err) 64 | } 65 | return value 66 | } 67 | 68 | // SetOrdinate sets the idx-th dim coordinate of s to val. 69 | func (s *CoordSeq) SetOrdinate(idx, dim int, val float64) { 70 | s.context.Lock() 71 | defer s.context.Unlock() 72 | if idx < 0 || s.size <= idx { 73 | panic(errIndexOutOfRange) 74 | } 75 | if dim < 0 || s.dimensions <= dim { 76 | panic(errDimensionOutOfRange) 77 | } 78 | if C.GEOSCoordSeq_setOrdinate_r(s.context.handle, s.s, C.uint(idx), C.uint(dim), C.double(val)) == 0 { 79 | panic(s.context.err) 80 | } 81 | } 82 | 83 | // SetX sets the idx-th X coordinate of s to val. 84 | func (s *CoordSeq) SetX(idx int, val float64) { 85 | s.context.Lock() 86 | defer s.context.Unlock() 87 | if idx < 0 || s.size <= idx { 88 | panic(errIndexOutOfRange) 89 | } 90 | if s.dimensions == 0 { 91 | panic(errDimensionOutOfRange) 92 | } 93 | if C.GEOSCoordSeq_setX_r(s.context.handle, s.s, C.uint(idx), C.double(val)) == 0 { 94 | panic(s.context.err) 95 | } 96 | } 97 | 98 | // SetY sets the idx-th Y coordinate of s to val. 99 | func (s *CoordSeq) SetY(idx int, val float64) { 100 | s.context.Lock() 101 | defer s.context.Unlock() 102 | if idx < 0 || s.size <= idx { 103 | panic(errIndexOutOfRange) 104 | } 105 | if s.dimensions < 2 { 106 | panic(errDimensionOutOfRange) 107 | } 108 | if C.GEOSCoordSeq_setY_r(s.context.handle, s.s, C.uint(idx), C.double(val)) == 0 { 109 | panic(s.context.err) 110 | } 111 | } 112 | 113 | // SetZ sets the idx-th Z coordinate of s to val. 114 | func (s *CoordSeq) SetZ(idx int, val float64) { 115 | s.context.Lock() 116 | defer s.context.Unlock() 117 | if idx < 0 || s.size <= idx { 118 | panic(errIndexOutOfRange) 119 | } 120 | if s.dimensions < 3 { 121 | panic(errDimensionOutOfRange) 122 | } 123 | if C.GEOSCoordSeq_setZ_r(s.context.handle, s.s, C.uint(idx), C.double(val)) == 0 { 124 | panic(s.context.err) 125 | } 126 | } 127 | 128 | // Size returns the size of s. 129 | func (s *CoordSeq) Size() int { 130 | return s.size 131 | } 132 | 133 | // ToCoords returns s as a [][]float64. 134 | func (s *CoordSeq) ToCoords() [][]float64 { 135 | s.context.Lock() 136 | defer s.context.Unlock() 137 | if s.size == 0 || s.dimensions == 0 { 138 | return nil 139 | } 140 | flatCoords := make([]float64, s.size*s.dimensions) 141 | var hasZ C.int 142 | if s.dimensions > 2 { 143 | hasZ = 1 144 | } 145 | var hasM C.int 146 | if s.dimensions > 3 { 147 | hasM = 1 148 | } 149 | if C.GEOSCoordSeq_copyToBuffer_r(s.context.handle, s.s, (*C.double)(&flatCoords[0]), hasZ, hasM) == 0 { 150 | panic(s.context.err) 151 | } 152 | coords := make([][]float64, s.size) 153 | j := 0 154 | for i := range s.size { 155 | coords[i] = flatCoords[j : j+s.dimensions : j+s.dimensions] 156 | j += s.dimensions 157 | } 158 | return coords 159 | } 160 | 161 | // X returns the idx-th X coordinate of s. 162 | func (s *CoordSeq) X(idx int) float64 { 163 | s.context.Lock() 164 | defer s.context.Unlock() 165 | if idx < 0 || s.size <= idx { 166 | panic(errIndexOutOfRange) 167 | } 168 | if s.dimensions == 0 { 169 | panic(errDimensionOutOfRange) 170 | } 171 | var val float64 172 | if C.GEOSCoordSeq_getX_r(s.context.handle, s.s, C.uint(idx), (*C.double)(&val)) == 0 { 173 | panic(s.context.err) 174 | } 175 | return val 176 | } 177 | 178 | // Y returns the idx-th Y coordinate of s. 179 | func (s *CoordSeq) Y(idx int) float64 { 180 | s.context.Lock() 181 | defer s.context.Unlock() 182 | if idx < 0 || s.size <= idx { 183 | panic(errIndexOutOfRange) 184 | } 185 | if s.dimensions < 2 { 186 | panic(errDimensionOutOfRange) 187 | } 188 | var val float64 189 | if C.GEOSCoordSeq_getY_r(s.context.handle, s.s, C.uint(idx), (*C.double)(&val)) == 0 { 190 | panic(s.context.err) 191 | } 192 | return val 193 | } 194 | 195 | // Z returns the idx-th Z coordinate of s. 196 | func (s *CoordSeq) Z(idx int) float64 { 197 | s.context.Lock() 198 | defer s.context.Unlock() 199 | if idx < 0 || s.size <= idx { 200 | panic(errIndexOutOfRange) 201 | } 202 | if s.dimensions < 3 { 203 | panic(errDimensionOutOfRange) 204 | } 205 | var val float64 206 | if C.GEOSCoordSeq_getZ_r(s.context.handle, s.s, C.uint(idx), (*C.double)(&val)) == 0 { 207 | panic(s.context.err) 208 | } 209 | return val 210 | } 211 | -------------------------------------------------------------------------------- /coordseq_test.go: -------------------------------------------------------------------------------- 1 | package geos_test 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "runtime" 7 | "testing" 8 | 9 | "github.com/alecthomas/assert/v2" 10 | 11 | "github.com/twpayne/go-geos" 12 | ) 13 | 14 | func TestCoordSeqAliasing(t *testing.T) { 15 | coords := geos.NewContext().NewCoordSeqFromCoords([][]float64{{0, 1}, {2, 3}}).ToCoords() 16 | coords[0] = append(coords[0], 4) 17 | assert.Equal(t, []float64{2, 3}, coords[1]) 18 | } 19 | 20 | func TestCoordSeqEmpty(t *testing.T) { 21 | defer runtime.GC() // Exercise finalizers. 22 | c := geos.NewContext() 23 | s := c.NewCoordSeq(0, 2) 24 | assert.Equal(t, 0, s.Size()) 25 | assert.Equal(t, 2, s.Dimensions()) 26 | assert.Equal(t, nil, s.ToCoords()) 27 | } 28 | 29 | func TestCoordSeqIsCCW(t *testing.T) { 30 | for _, tc := range []struct { 31 | name string 32 | coords [][]float64 33 | expected bool 34 | expectedErrPre13_2 bool 35 | }{ 36 | { 37 | name: "ccw", 38 | coords: [][]float64{{0, 0}, {1, 0}, {1, 1}, {0, 0}}, 39 | expected: true, 40 | }, 41 | { 42 | name: "cw", 43 | coords: [][]float64{{0, 0}, {0, 1}, {1, 1}, {0, 0}}, 44 | expected: false, 45 | }, 46 | { 47 | name: "short", 48 | coords: [][]float64{{0, 0}, {1, 0}, {1, 1}}, 49 | expected: false, 50 | expectedErrPre13_2: true, 51 | }, 52 | } { 53 | t.Run(tc.name, func(t *testing.T) { 54 | defer runtime.GC() // Exercise finalizers. 55 | s := geos.NewContext().NewCoordSeqFromCoords(tc.coords) 56 | if geos.VersionCompare(3, 12, 0) < 0 && tc.expectedErrPre13_2 { 57 | assert.Panics(t, func() { 58 | s.IsCCW() 59 | }) 60 | } else { 61 | assert.Equal(t, tc.expected, s.IsCCW()) 62 | } 63 | }) 64 | } 65 | } 66 | 67 | func TestCoordSeqMethods(t *testing.T) { 68 | defer runtime.GC() // Exercise finalizers. 69 | c := geos.NewContext() 70 | s := c.NewCoordSeq(2, 3) 71 | assert.Equal(t, 2, s.Size()) 72 | assert.Equal(t, 3, s.Dimensions()) 73 | assert.Equal(t, 0.0, s.X(0)) 74 | assert.Equal(t, 0.0, s.Y(0)) 75 | assert.True(t, math.IsNaN(s.Z(0))) 76 | s.SetZ(0, 0) 77 | s.SetX(1, 1) 78 | s.SetY(1, 2) 79 | s.SetZ(1, 3) 80 | assert.Equal(t, 1.0, s.X(1)) 81 | assert.Equal(t, 2.0, s.Y(1)) 82 | assert.Equal(t, 3.0, s.Z(1)) 83 | assert.Equal(t, 1.0, s.Ordinate(1, 0)) 84 | assert.Equal(t, 2.0, s.Ordinate(1, 1)) 85 | assert.Equal(t, 3.0, s.Ordinate(1, 2)) 86 | assert.Equal(t, [][]float64{{0, 0, 0}, {1, 2, 3}}, s.ToCoords()) 87 | 88 | clone := s.Clone() 89 | assert.Equal(t, 1.0, clone.X(1)) 90 | assert.Equal(t, 2.0, clone.Y(1)) 91 | clone.SetOrdinate(0, 0, -1.0) 92 | clone.SetOrdinate(0, 1, -2.0) 93 | assert.Equal(t, -1.0, clone.X(0)) 94 | assert.Equal(t, -2.0, clone.Y(0)) 95 | assert.Equal(t, [][]float64{{-1, -2, 0}, {1, 2, 3}}, clone.ToCoords()) 96 | 97 | assert.Equal(t, 3, clone.Dimensions()) 98 | assert.Equal(t, 3.0, clone.Z(1)) 99 | clone.SetOrdinate(0, 2, -3.0) 100 | assert.Equal(t, -3.0, clone.Z(0)) 101 | } 102 | 103 | func TestCoordSeqPanics(t *testing.T) { 104 | c := geos.NewContext() 105 | s := c.NewCoordSeq(1, 2) 106 | 107 | assert.Panics(t, func() { s.X(-1) }) 108 | assert.NotPanics(t, func() { s.X(0) }) 109 | assert.Panics(t, func() { s.X(1) }) 110 | 111 | assert.Panics(t, func() { s.Y(-1) }) 112 | assert.NotPanics(t, func() { s.Y(0) }) 113 | assert.Panics(t, func() { s.Y(1) }) 114 | 115 | assert.Panics(t, func() { s.Z(-1) }) 116 | assert.Panics(t, func() { s.Z(0) }) 117 | assert.Panics(t, func() { s.Z(1) }) 118 | 119 | assert.Panics(t, func() { s.SetX(-1, 0) }) 120 | assert.NotPanics(t, func() { s.SetX(0, 0) }) 121 | assert.Panics(t, func() { s.SetX(1, 0) }) 122 | 123 | assert.Panics(t, func() { s.SetY(-1, 0) }) 124 | assert.NotPanics(t, func() { s.SetY(0, 0) }) 125 | assert.Panics(t, func() { s.SetY(1, 0) }) 126 | 127 | assert.Panics(t, func() { s.SetZ(-1, 0) }) 128 | assert.Panics(t, func() { s.SetZ(0, 0) }) 129 | assert.Panics(t, func() { s.SetZ(1, 0) }) 130 | 131 | for idx := -1; idx <= 1; idx++ { 132 | for dim := -1; dim <= 4; dim++ { 133 | t.Run(fmt.Sprintf("idx_%d_dim_%d", idx, dim), func(t *testing.T) { 134 | if idx == 0 && 0 <= dim && dim < 2 { 135 | assert.NotPanics(t, func() { s.Ordinate(idx, dim) }) 136 | assert.NotPanics(t, func() { s.SetOrdinate(idx, dim, 0) }) 137 | } else { 138 | assert.Panics(t, func() { s.Ordinate(idx, dim) }) 139 | assert.Panics(t, func() { s.SetOrdinate(idx, dim, 0) }) 140 | } 141 | }) 142 | } 143 | } 144 | } 145 | 146 | func TestCoordSeqCoordsMethods(t *testing.T) { 147 | for _, tc := range []struct { 148 | name string 149 | coords [][]float64 150 | }{ 151 | { 152 | name: "point_2d", 153 | coords: [][]float64{{1, 2}}, 154 | }, 155 | { 156 | name: "point_3d", 157 | coords: [][]float64{{1, 2, 3}}, 158 | }, 159 | { 160 | name: "linestring_2d", 161 | coords: [][]float64{{1, 2}, {3, 4}}, 162 | }, 163 | { 164 | name: "linestring_3d", 165 | coords: [][]float64{{1, 2, 3}, {4, 5, 6}}, 166 | }, 167 | } { 168 | t.Run(tc.name, func(t *testing.T) { 169 | defer runtime.GC() // Exercise finalizers. 170 | c := geos.NewContext() 171 | s := c.NewCoordSeqFromCoords(tc.coords) 172 | assert.Equal(t, tc.coords, s.ToCoords()) 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /defaultcontext.go: -------------------------------------------------------------------------------- 1 | package geos 2 | 3 | // DefaultContext is the default context. 4 | var DefaultContext = NewContext() 5 | 6 | // Clone clones g into c. 7 | func Clone(g *Geom) *Geom { 8 | return DefaultContext.Clone(g) 9 | } 10 | 11 | // NewGeomFromBounds returns a new polygon populated with bounds. 12 | func NewGeomFromBounds(minX, minY, maxX, maxY float64) *Geom { 13 | return DefaultContext.NewGeomFromBounds(minX, minY, maxX, maxY) 14 | } 15 | 16 | // NewCollection returns a new collection. 17 | func NewCollection(typeID TypeID, geoms []*Geom) *Geom { 18 | return DefaultContext.NewCollection(typeID, geoms) 19 | } 20 | 21 | // NewCoordSeq returns a new CoordSeq. 22 | func NewCoordSeq(size, dims int) *CoordSeq { 23 | return DefaultContext.NewCoordSeq(size, dims) 24 | } 25 | 26 | // NewCoordSeqFromCoords returns a new CoordSeq populated with coords. 27 | func NewCoordSeqFromCoords(coords [][]float64) *CoordSeq { 28 | return DefaultContext.NewCoordSeqFromCoords(coords) 29 | } 30 | 31 | // NewEmptyCollection returns a new empty collection. 32 | func NewEmptyCollection(typeID TypeID) *Geom { 33 | return DefaultContext.NewEmptyCollection(typeID) 34 | } 35 | 36 | // NewEmptyLineString returns a new empty line string. 37 | func NewEmptyLineString() *Geom { 38 | return DefaultContext.NewEmptyLineString() 39 | } 40 | 41 | // NewEmptyPoint returns a new empty point. 42 | func NewEmptyPoint() *Geom { 43 | return DefaultContext.NewEmptyPoint() 44 | } 45 | 46 | // NewEmptyPolygon returns a new empty polygon. 47 | func NewEmptyPolygon() *Geom { 48 | return DefaultContext.NewEmptyPolygon() 49 | } 50 | 51 | // NewGeomFromGeoJSON parses a geometry in GeoJSON format from GeoJSON. 52 | func NewGeomFromGeoJSON(geoJSON string) (*Geom, error) { 53 | return DefaultContext.NewGeomFromGeoJSON(geoJSON) 54 | } 55 | 56 | // NewGeomFromWKB parses a geometry in WKB format from wkb. 57 | func NewGeomFromWKB(wkb []byte) (*Geom, error) { 58 | return DefaultContext.NewGeomFromWKB(wkb) 59 | } 60 | 61 | // NewGeomFromWKT parses a geometry in WKT format from wkt. 62 | func NewGeomFromWKT(wkt string) (*Geom, error) { 63 | return DefaultContext.NewGeomFromWKT(wkt) 64 | } 65 | 66 | // NewLinearRing returns a new linear ring populated with coords. 67 | func NewLinearRing(coords [][]float64) *Geom { 68 | return DefaultContext.NewLinearRing(coords) 69 | } 70 | 71 | // NewLineString returns a new line string populated with coords. 72 | func NewLineString(coords [][]float64) *Geom { 73 | return DefaultContext.NewLineString(coords) 74 | } 75 | 76 | // NewPoint returns a new point populated with coord. 77 | func NewPoint(coord []float64) *Geom { 78 | return DefaultContext.NewPoint(coord) 79 | } 80 | 81 | // NewPointFromXY returns a new point with x and y. 82 | func NewPointFromXY(x, y float64) *Geom { 83 | return DefaultContext.NewPointFromXY(x, y) 84 | } 85 | 86 | // NewPolygon returns a new point populated with coordss. 87 | func NewPolygon(coordss [][][]float64) *Geom { 88 | return DefaultContext.NewPolygon(coordss) 89 | } 90 | 91 | // Polygonize returns a set of geometries which contains linework that 92 | // represents the edges of a planar graph. 93 | func Polygonize(geoms []*Geom) *Geom { 94 | return DefaultContext.Polygonize(geoms) 95 | } 96 | 97 | // PolygonizeValid returns a set of polygons which contains linework that 98 | // represents the edges of a planar graph. 99 | func PolygonizeValid(geoms []*Geom) *Geom { 100 | return DefaultContext.PolygonizeValid(geoms) 101 | } 102 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/twpayne/go-geos/examples 2 | 3 | go 1.24.0 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/alecthomas/assert/v2 v2.11.0 9 | github.com/lib/pq v1.10.9 10 | github.com/testcontainers/testcontainers-go v0.35.0 11 | github.com/testcontainers/testcontainers-go/modules/postgres v0.35.0 12 | github.com/twpayne/go-geos v0.20.0 13 | ) 14 | 15 | require ( 16 | dario.cat/mergo v1.0.1 // indirect 17 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect 18 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect 19 | github.com/Microsoft/go-winio v0.6.2 // indirect 20 | github.com/alecthomas/repr v0.4.0 // indirect 21 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 22 | github.com/containerd/log v0.1.0 // indirect 23 | github.com/containerd/platforms v0.2.1 // indirect 24 | github.com/cpuguy83/dockercfg v0.3.2 // indirect 25 | github.com/davecgh/go-spew v1.1.1 // indirect 26 | github.com/distribution/reference v0.6.0 // indirect 27 | github.com/docker/docker v27.3.1+incompatible // indirect 28 | github.com/docker/go-connections v0.5.0 // indirect 29 | github.com/docker/go-units v0.5.0 // indirect 30 | github.com/felixge/httpsnoop v1.0.4 // indirect 31 | github.com/go-logr/logr v1.4.2 // indirect 32 | github.com/go-logr/stdr v1.2.2 // indirect 33 | github.com/go-ole/go-ole v1.3.0 // indirect 34 | github.com/gogo/protobuf v1.3.2 // indirect 35 | github.com/google/uuid v1.6.0 // indirect 36 | github.com/hexops/gotextdiff v1.0.3 // indirect 37 | github.com/klauspost/compress v1.17.11 // indirect 38 | github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect 39 | github.com/magiconair/properties v1.8.7 // indirect 40 | github.com/moby/docker-image-spec v1.3.1 // indirect 41 | github.com/moby/patternmatcher v0.6.0 // indirect 42 | github.com/moby/sys/sequential v0.6.0 // indirect 43 | github.com/moby/sys/user v0.3.0 // indirect 44 | github.com/moby/sys/userns v0.1.0 // indirect 45 | github.com/moby/term v0.5.0 // indirect 46 | github.com/morikuni/aec v1.0.0 // indirect 47 | github.com/opencontainers/go-digest v1.0.0 // indirect 48 | github.com/opencontainers/image-spec v1.1.0 // indirect 49 | github.com/pkg/errors v0.9.1 // indirect 50 | github.com/pmezard/go-difflib v1.0.0 // indirect 51 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect 52 | github.com/shirou/gopsutil/v3 v3.24.5 // indirect 53 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 54 | github.com/sirupsen/logrus v1.9.3 // indirect 55 | github.com/stretchr/testify v1.10.0 // indirect 56 | github.com/tklauser/go-sysconf v0.3.14 // indirect 57 | github.com/tklauser/numcpus v0.9.0 // indirect 58 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 59 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 60 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect 61 | go.opentelemetry.io/otel v1.35.0 // indirect 62 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 // indirect 63 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 64 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 65 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 66 | golang.org/x/crypto v0.35.0 // indirect 67 | golang.org/x/sys v0.30.0 // indirect 68 | golang.org/x/time v0.11.0 // indirect 69 | gopkg.in/yaml.v3 v3.0.1 // indirect 70 | ) 71 | 72 | replace github.com/twpayne/go-geos => .. 73 | -------------------------------------------------------------------------------- /examples/go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= 2 | dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= 4 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 5 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= 6 | github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 7 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 8 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 9 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 10 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 11 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 12 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 13 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 14 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 15 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 16 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 17 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 18 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 19 | github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= 20 | github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 21 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 22 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= 27 | github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 28 | github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= 29 | github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 30 | github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= 31 | github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= 32 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 33 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 34 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 35 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 36 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 37 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 38 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 39 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 40 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 41 | github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= 42 | github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 43 | github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 44 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 45 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 46 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 47 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 48 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 49 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 50 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 51 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= 52 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= 53 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 54 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 55 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 56 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 57 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= 58 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 59 | github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8= 60 | github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A= 61 | github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= 62 | github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 63 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 64 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 65 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 66 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 67 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 68 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 69 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 70 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 71 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 72 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 73 | github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0= 74 | github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= 75 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 76 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 77 | github.com/mdelapenya/tlscert v0.1.0 h1:YTpF579PYUX475eOL+6zyEO3ngLTOUWck78NBuJVXaM= 78 | github.com/mdelapenya/tlscert v0.1.0/go.mod h1:wrbyM/DwbFCeCeqdPX/8c6hNOqQgbf0rUDErE1uD+64= 79 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 80 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 81 | github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= 82 | github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 83 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 84 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 85 | github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= 86 | github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= 87 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 88 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 89 | github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= 90 | github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= 91 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 92 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 93 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 94 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 95 | github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= 96 | github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= 97 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 98 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 99 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 100 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 101 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= 102 | github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 103 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 104 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 105 | github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= 106 | github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= 107 | github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= 108 | github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 109 | github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= 110 | github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= 111 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 112 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 113 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 114 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 115 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 116 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 117 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 118 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 119 | github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo= 120 | github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4= 121 | github.com/testcontainers/testcontainers-go/modules/postgres v0.35.0 h1:eEGx9kYzZb2cNhRbBrNOCL/YPOM7+RMJiy3bB+ie0/I= 122 | github.com/testcontainers/testcontainers-go/modules/postgres v0.35.0/go.mod h1:hfH71Mia/WWLBgMD2YctYcMlfsbnT0hflweL1dy8Q4s= 123 | github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= 124 | github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY= 125 | github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo= 126 | github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI= 127 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 128 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 129 | github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= 130 | github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 131 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 132 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 133 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= 134 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= 135 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 136 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 137 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw= 138 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4= 139 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg= 140 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk= 141 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 142 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 143 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 144 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 145 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 146 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 147 | go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= 148 | go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 149 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 150 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 151 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 152 | golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= 153 | golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= 154 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 155 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 156 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 157 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 158 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 159 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 160 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 161 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 162 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 164 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 165 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 166 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 167 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 168 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 170 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 173 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 174 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 175 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 176 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 177 | golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= 178 | golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= 179 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 180 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 181 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= 182 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= 183 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 184 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 185 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 186 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 187 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 188 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 189 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 190 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 191 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 192 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 193 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a h1:nwKuGPlUAt+aR+pcrkfFRrTU1BVrSmYyYMxYbUIVHr0= 194 | google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a/go.mod h1:3kWAYMk1I75K4vykHtKt2ycnOgpA6974V7bREqbsenU= 195 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a h1:51aaUVRocpvUOSQKM6Q7VuoaktNIaMCLuhZB6DKksq4= 196 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a/go.mod h1:uRxBH1mhmO8PGhU89cMcHaXKZqO+OfakD8QQO0oYwlQ= 197 | google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg= 198 | google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 199 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 200 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 201 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 202 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 203 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 204 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 205 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 206 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 207 | gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= 208 | gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= 209 | -------------------------------------------------------------------------------- /examples/postgis/README.md: -------------------------------------------------------------------------------- 1 | # PostGIS example 2 | 3 | This example demonstrates: 4 | 5 | * Connecting to a PostgreSQL/PostGIS database. 6 | * Importing data in GeoJSON format and storing it in the database. 7 | * Exporting data from the database and converting it to GeoJSON. 8 | 9 | 10 | ## Quick start 11 | 12 | Change to this directory: 13 | 14 | ```console 15 | $ cd ${GOPATH}/src/github.com/twpayne/go-geos/examples/postgis 16 | ``` 17 | 18 | Create a database called `geomtest`: 19 | 20 | ```console 21 | $ createdb geomtest 22 | ``` 23 | 24 | Save the data source name in an environment variable, for example: 25 | 26 | ``` 27 | $ DSN="postgres://username:password@localhost/geomtest?binary_parameters=yes&sslmode=disable" 28 | ``` 29 | 30 | Create the database schema, including the PostGIS extension and a table with a 31 | geometry column: 32 | 33 | ```console 34 | $ go run . -dsn $DSN -create 35 | ``` 36 | 37 | Populate the database using [`pq.CopyIn`](https://pkg.go.dev/github.com/lib/pq#CopyIn): 38 | 39 | ```console 40 | $ go run . -dsn $DSN -populate 41 | ``` 42 | 43 | Write data from the database in GeoJSON format: 44 | 45 | ```console 46 | $ go run . -dsn $DSN -write 47 | {"id":1,"name":"London","geometry":{"type":"Point","coordinates":[0.1275,51.50722]}} 48 | {"id":2,"name":"Berlin","geometry":{"type":"Point","coordinates":[13.405,52.52]}} 49 | ``` 50 | 51 | Import new data into the database in GeoJSON format: 52 | 53 | ```console 54 | $ echo '{"name":"Paris","geometry":{"type":"Point","coordinates":[2.3508,48.8567]}}' | go run . -dsn $DSN -read 55 | ``` 56 | 57 | Verify that the data was imported: 58 | 59 | ```console 60 | $ go run . -dsn $DSN -write 61 | {"id":1,"name":"London","geometry":{"type":"Point","coordinates":[0.1275,51.50722]}} 62 | {"id":2,"name":"Berlin","geometry":{"type":"Point","coordinates":[13.405,52.52]}} 63 | {"id":3,"name":"Paris","geometry":{"type":"Point","coordinates":[2.3508,48.8567]}} 64 | ``` 65 | 66 | Delete the database: 67 | 68 | ```console 69 | $ dropdb geomtest 70 | ``` 71 | -------------------------------------------------------------------------------- /examples/postgis/main.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | 3 | package main 4 | 5 | import ( 6 | "database/sql" 7 | "encoding/json" 8 | "flag" 9 | "fmt" 10 | "io" 11 | "os" 12 | 13 | "github.com/lib/pq" 14 | 15 | "github.com/twpayne/go-geos" 16 | "github.com/twpayne/go-geos/geometry" 17 | ) 18 | 19 | var ( 20 | dsn = flag.String("dsn", "postgres://localhost/geomtest?binary_parameters=yes&sslmode=disable", "data source name") 21 | 22 | create = flag.Bool("create", false, "create database schema") 23 | populate = flag.Bool("populate", false, "populate waypoints") 24 | read = flag.Bool("read", false, "import waypoint from stdin in GeoJSON format") 25 | write = flag.Bool("write", false, "write waypoints to stdout in GeoJSON format") 26 | ) 27 | 28 | // A Waypoint is a location with an identifier and a name. 29 | type Waypoint struct { 30 | ID int `json:"id"` 31 | Name string `json:"name"` 32 | Geometry *geometry.Geometry `json:"geometry"` 33 | } 34 | 35 | // createDB demonstrates create a PostgreSQL/PostGIS database with a table with 36 | // a geometry column. 37 | func createDB(db *sql.DB) error { 38 | _, err := db.Exec(` 39 | CREATE EXTENSION IF NOT EXISTS postgis; 40 | CREATE TABLE IF NOT EXISTS waypoints ( 41 | id SERIAL PRIMARY KEY, 42 | name TEXT NOT NULL, 43 | geom geometry(POINT, 4326) NOT NULL 44 | ); 45 | `) 46 | return err 47 | } 48 | 49 | // populateDB demonstrates populating a PostgreSQL/PostGIS database using 50 | // pq.CopyIn for fast imports. 51 | func populateDB(db *sql.DB) error { 52 | tx, err := db.Begin() 53 | if err != nil { 54 | return err 55 | } 56 | defer tx.Rollback() 57 | stmt, err := tx.Prepare(pq.CopyIn("waypoints", "name", "geom")) 58 | if err != nil { 59 | return err 60 | } 61 | for _, waypoint := range []Waypoint{ 62 | { 63 | Name: "London", 64 | Geometry: geometry.NewGeometry(geos.NewPoint([]float64{0.1275, 51.50722}).SetSRID(4326)), 65 | }, 66 | { 67 | Name: "Berlin", 68 | Geometry: geometry.NewGeometry(geos.NewPoint([]float64{13.405, 52.52}).SetSRID(4326)), 69 | }, 70 | } { 71 | if _, err := stmt.Exec(waypoint.Name, waypoint.Geometry); err != nil { 72 | return err 73 | } 74 | } 75 | if _, err := stmt.Exec(); err != nil { 76 | return err 77 | } 78 | return tx.Commit() 79 | } 80 | 81 | // readGeoJSON demonstrates reading GeoJSON data and inserting it into a 82 | // database with INSERT. 83 | func readGeoJSON(db *sql.DB, r io.Reader) error { 84 | var waypoint Waypoint 85 | if err := json.NewDecoder(r).Decode(&waypoint); err != nil { 86 | return err 87 | } 88 | _, err := db.Exec(` 89 | INSERT INTO waypoints(name, geom) VALUES ($1, $2); 90 | `, waypoint.Name, waypoint.Geometry) 91 | return err 92 | } 93 | 94 | // writeGeoJSON demonstrates reading data from a database with SELECT and 95 | // writing it as GeoJSON. 96 | func writeGeoJSON(db *sql.DB, w io.Writer) error { 97 | rows, err := db.Query(` 98 | SELECT id, name, ST_AsEWKB(geom) FROM waypoints ORDER BY id ASC; 99 | `) 100 | if err != nil { 101 | return err 102 | } 103 | defer rows.Close() 104 | for rows.Next() { 105 | var waypoint Waypoint 106 | if err := rows.Scan(&waypoint.ID, &waypoint.Name, &waypoint.Geometry); err != nil { 107 | return err 108 | } 109 | if err := json.NewEncoder(w).Encode(&waypoint); err != nil { 110 | return err 111 | } 112 | } 113 | return rows.Err() 114 | } 115 | 116 | func run() error { 117 | flag.Parse() 118 | db, err := sql.Open("postgres", *dsn) 119 | if err != nil { 120 | return err 121 | } 122 | defer db.Close() 123 | if err := db.Ping(); err != nil { 124 | return err 125 | } 126 | if *create { 127 | if err := createDB(db); err != nil { 128 | return err 129 | } 130 | } 131 | if *populate { 132 | if err := populateDB(db); err != nil { 133 | return err 134 | } 135 | } 136 | if *read { 137 | if err := readGeoJSON(db, os.Stdin); err != nil { 138 | return err 139 | } 140 | } 141 | if *write { 142 | if err := writeGeoJSON(db, os.Stdout); err != nil { 143 | return err 144 | } 145 | } 146 | return nil 147 | } 148 | 149 | func main() { 150 | if err := run(); err != nil { 151 | fmt.Println(err) 152 | os.Exit(1) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /examples/postgis/main_test.go: -------------------------------------------------------------------------------- 1 | //go:build go1.21 2 | 3 | package main 4 | 5 | import ( 6 | "bytes" 7 | "context" 8 | "database/sql" 9 | "os/exec" 10 | "strings" 11 | "testing" 12 | "time" 13 | 14 | "github.com/alecthomas/assert/v2" 15 | _ "github.com/lib/pq" 16 | "github.com/testcontainers/testcontainers-go" 17 | "github.com/testcontainers/testcontainers-go/modules/postgres" 18 | "github.com/testcontainers/testcontainers-go/wait" 19 | ) 20 | 21 | func TestIntegration(t *testing.T) { 22 | ctx := context.Background() 23 | 24 | if _, err := exec.LookPath("docker"); err != nil { 25 | t.Skip("docker not found in $PATH") 26 | } 27 | 28 | var ( 29 | database = "testdb" 30 | user = "testuser" 31 | password = "testpassword" 32 | ) 33 | 34 | pgContainer, err := postgres.RunContainer(ctx, 35 | testcontainers.WithImage("docker.io/postgis/postgis:16-3.4"), 36 | postgres.WithDatabase(database), 37 | postgres.WithUsername(user), 38 | postgres.WithPassword(password), 39 | testcontainers.WithWaitStrategy( 40 | wait.ForLog("database system is ready to accept connections"). 41 | WithOccurrence(2). 42 | WithStartupTimeout(5*time.Second), 43 | ), 44 | ) 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | 49 | t.Cleanup(func() { 50 | assert.NoError(t, pgContainer.Terminate(ctx)) 51 | }) 52 | 53 | connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable") 54 | assert.NoError(t, err) 55 | 56 | db, err := sql.Open("postgres", connStr) 57 | assert.NoError(t, err) 58 | defer func() { 59 | assert.NoError(t, db.Close()) 60 | }() 61 | 62 | assert.NoError(t, createDB(db)) 63 | 64 | assert.NoError(t, populateDB(db)) 65 | 66 | r := bytes.NewBufferString(`{"name":"Paris","geometry":{"type":"Point","coordinates":[2.3508,48.8567]}}`) 67 | assert.NoError(t, readGeoJSON(db, r)) 68 | 69 | w := &strings.Builder{} 70 | assert.NoError(t, writeGeoJSON(db, w)) 71 | assert.Equal(t, strings.Join([]string{ 72 | `{"id":1,"name":"London","geometry":{"type":"Point","coordinates":[0.1275,51.50722]}}`, 73 | `{"id":2,"name":"Berlin","geometry":{"type":"Point","coordinates":[13.405,52.52]}}`, 74 | `{"id":3,"name":"Paris","geometry":{"type":"Point","coordinates":[2.3508,48.8567]}}`, 75 | }, "\n")+"\n", w.String()) 76 | } 77 | -------------------------------------------------------------------------------- /geojson/geojson.go: -------------------------------------------------------------------------------- 1 | // Package geojson implements GEOS-backed GeoJSON. 2 | package geojson 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/twpayne/go-geos/geometry" 9 | ) 10 | 11 | const ( 12 | featureType = "Feature" 13 | featureCollectionType = "FeatureCollection" 14 | ) 15 | 16 | // A Feature is a feature. 17 | type Feature struct { 18 | ID any 19 | Geometry geometry.Geometry 20 | Properties map[string]any 21 | } 22 | 23 | // A FeatureCollection is a feature collection. 24 | type FeatureCollection []*Feature 25 | 26 | type feature struct { 27 | ID any `json:"id,omitempty"` 28 | Type string `json:"type"` 29 | Geometry *geometry.Geometry `json:"geometry"` 30 | Properties map[string]any `json:"properties,omitempty"` 31 | } 32 | 33 | type featureCollection struct { 34 | Type string `json:"type"` 35 | Features []feature `json:"features"` 36 | } 37 | 38 | // MarshalJSON implements json.Marshaler. 39 | func (f *Feature) MarshalJSON() ([]byte, error) { 40 | return json.Marshal(feature{ 41 | ID: f.ID, 42 | Type: featureType, 43 | Geometry: &f.Geometry, 44 | Properties: f.Properties, 45 | }) 46 | } 47 | 48 | // UnmarshalJSON implements json.Unmarshaler. 49 | func (f *Feature) UnmarshalJSON(data []byte) error { 50 | var geoJSONFeature feature 51 | if err := json.Unmarshal(data, &geoJSONFeature); err != nil { 52 | return err 53 | } 54 | if geoJSONFeature.Type != featureType { 55 | return fmt.Errorf("not a Feature: %s", geoJSONFeature.Type) 56 | } 57 | f.ID = geoJSONFeature.ID 58 | f.Geometry = *geoJSONFeature.Geometry 59 | f.Properties = geoJSONFeature.Properties 60 | return nil 61 | } 62 | 63 | // MarshalJSON implements json.Marshaler. 64 | func (fc FeatureCollection) MarshalJSON() ([]byte, error) { 65 | features := make([]feature, 0, len(fc)) 66 | for _, f := range fc { 67 | feature := feature{ 68 | ID: f.ID, 69 | Type: featureType, 70 | Geometry: &f.Geometry, 71 | Properties: f.Properties, 72 | } 73 | features = append(features, feature) 74 | } 75 | return json.Marshal(featureCollection{ 76 | Type: featureCollectionType, 77 | Features: features, 78 | }) 79 | } 80 | 81 | // UnmarshalJSON implements json.Unmarshaler. 82 | func (fc *FeatureCollection) UnmarshalJSON(data []byte) error { 83 | var geoJSONFeatureCollection featureCollection 84 | if err := json.Unmarshal(data, &geoJSONFeatureCollection); err != nil { 85 | return err 86 | } 87 | if geoJSONFeatureCollection.Type != featureCollectionType { 88 | return fmt.Errorf("not a FeatureCollection: %s", geoJSONFeatureCollection.Type) 89 | } 90 | featureCollection := make([]*Feature, len(geoJSONFeatureCollection.Features)) 91 | for i := range featureCollection { 92 | feature := geoJSONFeatureCollection.Features[i] 93 | if feature.Type != featureType { 94 | return fmt.Errorf("not a Feature: %s", feature.Type) 95 | } 96 | f := &Feature{ 97 | ID: feature.ID, 98 | Geometry: *feature.Geometry, 99 | Properties: feature.Properties, 100 | } 101 | featureCollection[i] = f 102 | } 103 | *fc = featureCollection 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /geojson/geojson_test.go: -------------------------------------------------------------------------------- 1 | package geojson_test 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | 9 | "github.com/twpayne/go-geos" 10 | "github.com/twpayne/go-geos/geojson" 11 | "github.com/twpayne/go-geos/geometry" 12 | ) 13 | 14 | func TestFeature(t *testing.T) { 15 | for i, tc := range []struct { 16 | feat *geojson.Feature 17 | geoJSONStr string 18 | }{ 19 | { 20 | feat: &geojson.Feature{ 21 | ID: "testID", 22 | Geometry: *geometry.NewGeometry(geos.NewPoint([]float64{1, 2})), 23 | Properties: map[string]any{ 24 | "key": "value", 25 | }, 26 | }, 27 | geoJSONStr: `{"id":"testID","type":"Feature","geometry":{"type":"Point","coordinates":[1,2]},"properties":{"key":"value"}}`, 28 | }, 29 | } { 30 | t.Run(strconv.Itoa(i), func(t *testing.T) { 31 | actualGeoJSON, err := tc.feat.MarshalJSON() 32 | assert.NoError(t, err) 33 | assert.Equal(t, tc.geoJSONStr, string(actualGeoJSON)) 34 | 35 | var feat geojson.Feature 36 | assert.NoError(t, feat.UnmarshalJSON([]byte(tc.geoJSONStr))) 37 | assert.True(t, tc.feat.Geometry.Equals(feat.Geometry.Geom)) 38 | }) 39 | } 40 | } 41 | 42 | func TestFeatureCollection(t *testing.T) { 43 | for i, tc := range []struct { 44 | featureCollection geojson.FeatureCollection 45 | geoJSONStr string 46 | }{ 47 | { 48 | featureCollection: geojson.FeatureCollection{}, 49 | geoJSONStr: `{"type":"FeatureCollection","features":[]}`, 50 | }, 51 | { 52 | featureCollection: geojson.FeatureCollection{ 53 | { 54 | ID: "point", 55 | Geometry: *geometry.NewGeometry(geos.NewPoint([]float64{1, 2})), 56 | Properties: map[string]any{ 57 | "key": "value", 58 | }, 59 | }, 60 | }, 61 | geoJSONStr: `{"type":"FeatureCollection","features":[{"id":"point","type":"Feature","geometry":{"type":"Point","coordinates":[1,2]},"properties":{"key":"value"}}]}`, 62 | }, 63 | { 64 | featureCollection: geojson.FeatureCollection{ 65 | { 66 | ID: "point", 67 | Geometry: *geometry.NewGeometry(geos.NewPoint([]float64{1, 2})), 68 | Properties: map[string]any{ 69 | "key": "value", 70 | }, 71 | }, 72 | { 73 | ID: "linestring", 74 | Geometry: *geometry.NewGeometry(geos.NewLineString([][]float64{{1, 2}, {3, 4}})), 75 | Properties: map[string]any{ 76 | "key": "value", 77 | }, 78 | }, 79 | }, 80 | geoJSONStr: `{"type":"FeatureCollection","features":[{"id":"point","type":"Feature","geometry":{"type":"Point","coordinates":[1,2]},"properties":{"key":"value"}},{"id":"linestring","type":"Feature","geometry":{"type":"LineString","coordinates":[[1,2],[3,4]]},"properties":{"key":"value"}}]}`, 81 | }, 82 | } { 83 | t.Run(strconv.Itoa(i), func(t *testing.T) { 84 | actualGeoJSON, err := tc.featureCollection.MarshalJSON() 85 | assert.NoError(t, err) 86 | assert.Equal(t, tc.geoJSONStr, string(actualGeoJSON)) 87 | 88 | var featureCollection geojson.FeatureCollection 89 | assert.NoError(t, featureCollection.UnmarshalJSON([]byte(tc.geoJSONStr))) 90 | assert.Equal(t, tc.featureCollection, featureCollection) 91 | }) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /geom.go: -------------------------------------------------------------------------------- 1 | //go:generate go tool execute-template -data geommethods.yaml -output geommethods.go geommethods.go.tmpl 2 | 3 | package geos 4 | 5 | // #include 6 | // #include "go-geos.h" 7 | import "C" 8 | 9 | import ( 10 | "unsafe" 11 | ) 12 | 13 | // A Geom is a geometry. 14 | type Geom struct { 15 | context *Context 16 | geom *C.struct_GEOSGeom_t 17 | parent *Geom 18 | typeID TypeID 19 | numGeometries int 20 | numInteriorRings int 21 | numPoints int 22 | } 23 | 24 | // Destroy destroys g and releases all resources it holds. 25 | func (g *Geom) Destroy() { 26 | // Protect against Destroy being called more than once. 27 | if g == nil || g.context == nil { 28 | return 29 | } 30 | if g.parent == nil { 31 | g.context.Lock() 32 | defer g.context.Unlock() 33 | C.GEOSGeom_destroy_r(g.context.handle, g.geom) 34 | } 35 | *g = Geom{} // Clear all references. 36 | } 37 | 38 | // Bounds returns g's bounds. 39 | func (g *Geom) Bounds() *Box2D { 40 | g.mustNotBeDestroyed() 41 | bounds := NewBox2DEmpty() 42 | g.context.Lock() 43 | defer g.context.Unlock() 44 | C.c_GEOSGeomBounds_r(g.context.handle, g.geom, (*C.double)(&bounds.MinX), (*C.double)(&bounds.MinY), (*C.double)(&bounds.MaxX), (*C.double)(&bounds.MaxY)) 45 | return bounds 46 | } 47 | 48 | // MakeValidWithParams returns a new valid geometry using the MakeValidMethods and MakeValidCollapsed parameters. 49 | func (g *Geom) MakeValidWithParams(method MakeValidMethod, collapse MakeValidCollapsed) *Geom { 50 | g.mustNotBeDestroyed() 51 | g.context.Lock() 52 | defer g.context.Unlock() 53 | cRes := C.c_GEOSMakeValidWithParams_r(g.context.handle, g.geom, C.enum_GEOSMakeValidMethods(method), C.int(collapse)) 54 | return g.context.newGeom(cRes, nil) 55 | } 56 | 57 | // BufferWithParams returns g buffered with bufferParams. 58 | func (g *Geom) BufferWithParams(bufferParams *BufferParams, width float64) *Geom { 59 | g.context.Lock() 60 | defer g.context.Unlock() 61 | if bufferParams.context != g.context { 62 | bufferParams.context.Lock() 63 | defer bufferParams.context.Unlock() 64 | } 65 | return g.context.newNonNilGeom(C.GEOSBufferWithParams_r(g.context.handle, g.geom, bufferParams.bufferParams, C.double(width)), nil) 66 | } 67 | 68 | func (g *Geom) ClipByBox2D(box2d *Box2D) *Geom { 69 | return g.ClipByRect(box2d.MinX, box2d.MinY, box2d.MaxX, box2d.MaxY) 70 | } 71 | 72 | // CoordSeq returns g's coordinate sequence. 73 | func (g *Geom) CoordSeq() *CoordSeq { 74 | g.mustNotBeDestroyed() 75 | g.context.Lock() 76 | defer g.context.Unlock() 77 | s := C.GEOSGeom_getCoordSeq_r(g.context.handle, g.geom) 78 | // Don't set a finalizer as coordSeq is owned by g and will be finalized when g is 79 | // finalized. 80 | coordSeq := g.context.newCoordSeqInternal(s, nil) 81 | if coordSeq == nil { 82 | return nil 83 | } 84 | coordSeq.parent = g 85 | return coordSeq 86 | } 87 | 88 | // ExteriorRing returns the exterior ring. 89 | func (g *Geom) ExteriorRing() *Geom { 90 | g.mustNotBeDestroyed() 91 | g.context.Lock() 92 | defer g.context.Unlock() 93 | return g.context.newNonNilGeom(C.GEOSGetExteriorRing_r(g.context.handle, g.geom), g) 94 | } 95 | 96 | // Geometry returns the nth geometry of g. 97 | func (g *Geom) Geometry(n int) *Geom { 98 | g.mustNotBeDestroyed() 99 | g.context.Lock() 100 | defer g.context.Unlock() 101 | if n < 0 || g.numGeometries <= n { 102 | panic(errIndexOutOfRange) 103 | } 104 | return g.context.newNonNilGeom(C.GEOSGetGeometryN_r(g.context.handle, g.geom, C.int(n)), g) 105 | } 106 | 107 | // InteriorRing returns the nth interior ring. 108 | func (g *Geom) InteriorRing(n int) *Geom { 109 | g.mustNotBeDestroyed() 110 | g.context.Lock() 111 | defer g.context.Unlock() 112 | if n < 0 || g.numInteriorRings <= n { 113 | panic(errIndexOutOfRange) 114 | } 115 | return g.context.newNonNilGeom(C.GEOSGetInteriorRingN_r(g.context.handle, g.geom, C.int(n)), g) 116 | } 117 | 118 | // IsValidReason returns the reason that g is invalid. 119 | func (g *Geom) IsValidReason() string { 120 | g.mustNotBeDestroyed() 121 | g.context.Lock() 122 | defer g.context.Unlock() 123 | reason := C.GEOSisValidReason_r(g.context.handle, g.geom) 124 | if reason == nil { 125 | panic(g.context.err) 126 | } 127 | defer C.GEOSFree_r(g.context.handle, unsafe.Pointer(reason)) 128 | return C.GoString(reason) 129 | } 130 | 131 | // NearestPoints returns the nearest coordinates of g and other. If the nearest 132 | // coordinates do not exist (e.g., when either geom is empty), it returns nil. 133 | func (g *Geom) NearestPoints(other *Geom) [][]float64 { 134 | g.mustNotBeDestroyed() 135 | g.context.Lock() 136 | defer g.context.Unlock() 137 | s := C.GEOSNearestPoints_r(g.context.handle, g.geom, other.geom) 138 | if s == nil { 139 | return nil 140 | } 141 | defer C.GEOSCoordSeq_destroy_r(g.context.handle, s) 142 | return g.context.newCoordsFromGEOSCoordSeq(s) 143 | } 144 | 145 | func (g *Geom) Normalize() *Geom { 146 | g.mustNotBeDestroyed() 147 | g.context.Lock() 148 | defer g.context.Unlock() 149 | if C.GEOSNormalize_r(g.context.handle, g.geom) != 0 { 150 | panic(g.context.err) 151 | } 152 | return g 153 | } 154 | 155 | // NumCoordinates returns the number of coordinates in g. 156 | func (g *Geom) NumCoordinates() int { 157 | g.mustNotBeDestroyed() 158 | g.context.Lock() 159 | defer g.context.Unlock() 160 | numCoordinates := C.GEOSGetNumCoordinates_r(g.context.handle, g.geom) 161 | if numCoordinates == -1 { 162 | panic(g.context.err) 163 | } 164 | return int(numCoordinates) 165 | } 166 | 167 | // NumGeometries returns the number of geometries in g. 168 | func (g *Geom) NumGeometries() int { 169 | g.mustNotBeDestroyed() 170 | return g.numGeometries 171 | } 172 | 173 | // NumInteriorRings returns the number of interior rings in g. 174 | func (g *Geom) NumInteriorRings() int { 175 | g.mustNotBeDestroyed() 176 | return g.numInteriorRings 177 | } 178 | 179 | // NumPoints returns the number of points in g. 180 | func (g *Geom) NumPoints() int { 181 | g.mustNotBeDestroyed() 182 | return g.numPoints 183 | } 184 | 185 | // Point returns the g's nth point. 186 | func (g *Geom) Point(n int) *Geom { 187 | g.mustNotBeDestroyed() 188 | g.context.Lock() 189 | defer g.context.Unlock() 190 | if n < 0 || g.numPoints <= n { 191 | panic(errIndexOutOfRange) 192 | } 193 | return g.context.newNonNilGeom(C.GEOSGeomGetPointN_r(g.context.handle, g.geom, C.int(n)), nil) 194 | } 195 | 196 | // PolygonizeFull returns a set of geometries which contains linework that 197 | // represents the edge of a planar graph. 198 | func (g *Geom) PolygonizeFull() (geom, cuts, dangles, invalidRings *Geom) { 199 | g.mustNotBeDestroyed() 200 | g.context.Lock() 201 | defer g.context.Unlock() 202 | var cCuts, cDangles, cInvalidRings *C.struct_GEOSGeom_t 203 | cGeom := C.GEOSPolygonize_full_r(g.context.handle, g.geom, &cCuts, &cDangles, &cInvalidRings) //nolint:gocritic 204 | geom = g.context.newNonNilGeom(cGeom, nil) 205 | cuts = g.context.newGeom(cCuts, nil) 206 | dangles = g.context.newGeom(cDangles, nil) 207 | invalidRings = g.context.newGeom(cInvalidRings, nil) 208 | return geom, cuts, dangles, invalidRings 209 | } 210 | 211 | // Precision returns g's precision. 212 | func (g *Geom) Precision() float64 { 213 | g.mustNotBeDestroyed() 214 | g.context.Lock() 215 | defer g.context.Unlock() 216 | return float64(C.GEOSGeom_getPrecision_r(g.context.handle, g.geom)) 217 | } 218 | 219 | // RelatePattern returns if the DE9IM pattern for g and other matches pat. 220 | func (g *Geom) RelatePattern(other *Geom, pat string) bool { 221 | g.mustNotBeDestroyed() 222 | patCStr := C.CString(pat) 223 | defer C.free(unsafe.Pointer(patCStr)) 224 | g.context.Lock() 225 | defer g.context.Unlock() 226 | switch C.GEOSRelatePattern_r(g.context.handle, g.geom, other.geom, patCStr) { 227 | case 0: 228 | return false 229 | case 1: 230 | return true 231 | default: 232 | panic(g.context.err) 233 | } 234 | } 235 | 236 | // SRID returns g's SRID. 237 | func (g *Geom) SRID() int { 238 | g.mustNotBeDestroyed() 239 | g.context.Lock() 240 | defer g.context.Unlock() 241 | srid := C.GEOSGetSRID_r(g.context.handle, g.geom) 242 | // geos_c.h states that GEOSGetSRID_r "Return 0 on exception" but 0 is also 243 | // returned if the SRID is not set, so we can't rely on it to propagate 244 | // exceptions. 245 | return int(srid) 246 | } 247 | 248 | // SetSRID sets g's SRID to srid. 249 | func (g *Geom) SetSRID(srid int) *Geom { 250 | g.mustNotBeDestroyed() 251 | g.context.Lock() 252 | defer g.context.Unlock() 253 | C.GEOSSetSRID_r(g.context.handle, g.geom, C.int(srid)) 254 | return g 255 | } 256 | 257 | // SetUserData sets g's userdata and returns g. 258 | func (g *Geom) SetUserData(userdata uintptr) *Geom { 259 | g.mustNotBeDestroyed() 260 | g.context.Lock() 261 | defer g.context.Unlock() 262 | C.c_GEOSGeom_setUserData_r(g.context.handle, g.geom, C.uintptr_t(userdata)) 263 | return g 264 | } 265 | 266 | // String returns g in WKT format. 267 | func (g *Geom) String() string { 268 | g.mustNotBeDestroyed() 269 | return g.ToWKT() 270 | } 271 | 272 | // ToEWKBWithSRID returns g in Extended WKB format with its SRID. 273 | func (g *Geom) ToEWKBWithSRID() []byte { 274 | g.mustNotBeDestroyed() 275 | g.context.Lock() 276 | defer g.context.Unlock() 277 | if g.context.ewkbWithSRIDWriter == nil { 278 | g.context.ewkbWithSRIDWriter = C.GEOSWKBWriter_create_r(g.context.handle) 279 | C.GEOSWKBWriter_setFlavor_r(g.context.handle, g.context.ewkbWithSRIDWriter, C.GEOS_WKB_EXTENDED) 280 | C.GEOSWKBWriter_setIncludeSRID_r(g.context.handle, g.context.ewkbWithSRIDWriter, 1) 281 | } 282 | var size C.size_t 283 | ewkbCBuf := C.GEOSWKBWriter_write_r(g.context.handle, g.context.ewkbWithSRIDWriter, g.geom, &size) 284 | defer C.GEOSFree_r(g.context.handle, unsafe.Pointer(ewkbCBuf)) 285 | return C.GoBytes(unsafe.Pointer(ewkbCBuf), C.int(size)) 286 | } 287 | 288 | // ToGeoJSON returns g in GeoJSON format. 289 | func (g *Geom) ToGeoJSON(indent int) string { 290 | g.mustNotBeDestroyed() 291 | g.context.Lock() 292 | defer g.context.Unlock() 293 | if g.context.geoJSONWriter == nil { 294 | g.context.geoJSONWriter = C.GEOSGeoJSONWriter_create_r(g.context.handle) 295 | } 296 | geoJSONCStr := C.GEOSGeoJSONWriter_writeGeometry_r(g.context.handle, g.context.geoJSONWriter, g.geom, C.int(indent)) 297 | defer C.GEOSFree_r(g.context.handle, unsafe.Pointer(geoJSONCStr)) 298 | return C.GoString(geoJSONCStr) 299 | } 300 | 301 | // ToWKB returns g in WKB format. 302 | func (g *Geom) ToWKB() []byte { 303 | g.mustNotBeDestroyed() 304 | g.context.Lock() 305 | defer g.context.Unlock() 306 | if g.context.wkbWriter == nil { 307 | g.context.wkbWriter = C.GEOSWKBWriter_create_r(g.context.handle) 308 | } 309 | var size C.size_t 310 | wkbCBuf := C.GEOSWKBWriter_write_r(g.context.handle, g.context.wkbWriter, g.geom, &size) 311 | defer C.GEOSFree_r(g.context.handle, unsafe.Pointer(wkbCBuf)) 312 | return C.GoBytes(unsafe.Pointer(wkbCBuf), C.int(size)) 313 | } 314 | 315 | // ToWKT returns g in WKT format. 316 | func (g *Geom) ToWKT() string { 317 | g.mustNotBeDestroyed() 318 | g.context.Lock() 319 | defer g.context.Unlock() 320 | if g.context.wktWriter == nil { 321 | g.context.wktWriter = C.GEOSWKTWriter_create_r(g.context.handle) 322 | } 323 | wktCStr := C.GEOSWKTWriter_write_r(g.context.handle, g.context.wktWriter, g.geom) 324 | defer C.GEOSFree_r(g.context.handle, unsafe.Pointer(wktCStr)) 325 | return C.GoString(wktCStr) 326 | } 327 | 328 | // Type returns g's type. 329 | func (g *Geom) Type() string { 330 | g.mustNotBeDestroyed() 331 | g.context.Lock() 332 | defer g.context.Unlock() 333 | typeCStr := C.GEOSGeomType_r(g.context.handle, g.geom) 334 | if typeCStr == nil { 335 | panic(g.context.err) 336 | } 337 | defer C.GEOSFree_r(g.context.handle, unsafe.Pointer(typeCStr)) 338 | return C.GoString(typeCStr) 339 | } 340 | 341 | // TypeID returns g's geometry type id. 342 | func (g *Geom) TypeID() TypeID { 343 | g.mustNotBeDestroyed() 344 | return g.typeID 345 | } 346 | 347 | // UserData returns g's userdata. 348 | func (g *Geom) UserData() uintptr { 349 | g.mustNotBeDestroyed() 350 | g.context.Lock() 351 | defer g.context.Unlock() 352 | return uintptr(C.c_GEOSGeom_getUserData_r(g.context.handle, g.geom)) 353 | } 354 | 355 | func (g *Geom) finalize() { 356 | if g.context == nil { 357 | return 358 | } 359 | if g.context.geomFinalizeFunc != nil { 360 | g.context.geomFinalizeFunc(g) 361 | } 362 | g.Destroy() 363 | } 364 | 365 | func (g *Geom) mustNotBeDestroyed() { 366 | if g.context == nil { 367 | panic("destroyed Geom") 368 | } 369 | } 370 | -------------------------------------------------------------------------------- /geometry/binary.go: -------------------------------------------------------------------------------- 1 | package geometry 2 | 3 | import "github.com/twpayne/go-geos" 4 | 5 | // NewGeometryFromWKB returns a new Geometry from wkb. 6 | func NewGeometryFromWKB(wkb []byte) (*Geometry, error) { 7 | geom, err := geos.NewGeomFromWKB(wkb) 8 | if err != nil { 9 | return nil, err 10 | } 11 | return &Geometry{Geom: geom}, nil 12 | } 13 | 14 | // MarshalBinary implements encoding.BinaryMarshaler. 15 | func (g *Geometry) MarshalBinary() ([]byte, error) { 16 | return g.ToEWKBWithSRID(), nil 17 | } 18 | 19 | // UnmarshalBinary implements encoding.BinaryUnmarshaler. 20 | func (g *Geometry) UnmarshalBinary(data []byte) error { 21 | geom, err := geos.NewGeomFromWKB(data) 22 | if err != nil { 23 | return err 24 | } 25 | g.Geom = geom 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /geometry/binary_test.go: -------------------------------------------------------------------------------- 1 | package geometry_test 2 | 3 | import ( 4 | "encoding" 5 | "encoding/hex" 6 | "strconv" 7 | "testing" 8 | 9 | "github.com/alecthomas/assert/v2" 10 | 11 | "github.com/twpayne/go-geos" 12 | "github.com/twpayne/go-geos/geometry" 13 | ) 14 | 15 | var ( 16 | _ encoding.BinaryMarshaler = &geometry.Geometry{} 17 | _ encoding.BinaryUnmarshaler = &geometry.Geometry{} 18 | ) 19 | 20 | func TestBinary(t *testing.T) { 21 | for i, tc := range []struct { 22 | geom *geometry.Geometry 23 | binaryStr string 24 | }{ 25 | { 26 | geom: geometry.NewGeometry(geos.NewPoint([]float64{1, 2})), 27 | binaryStr: "0101000000000000000000f03f0000000000000040", 28 | }, 29 | } { 30 | t.Run(strconv.Itoa(i), func(t *testing.T) { 31 | actualBinary, err := tc.geom.MarshalBinary() 32 | assert.NoError(t, err) 33 | assert.Equal(t, tc.binaryStr, hex.EncodeToString(actualBinary)) 34 | 35 | var geom geometry.Geometry 36 | binary, err := hex.DecodeString(tc.binaryStr) 37 | assert.NoError(t, err) 38 | assert.NoError(t, geom.UnmarshalBinary(binary)) 39 | assert.True(t, tc.geom.Equals(geom.Geom)) 40 | }) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /geometry/bounds_test.go: -------------------------------------------------------------------------------- 1 | package geometry_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | 8 | "github.com/twpayne/go-geos" 9 | ) 10 | 11 | func TestBounds(t *testing.T) { 12 | for _, tc := range []struct { 13 | name string 14 | bounds *geos.Box2D 15 | expectedEmpty bool 16 | expectedGeomWKT string 17 | }{ 18 | { 19 | name: "NewBoundsEmpty", 20 | bounds: geos.NewBox2DEmpty(), 21 | expectedEmpty: true, 22 | expectedGeomWKT: "POINT EMPTY", 23 | }, 24 | { 25 | name: "NewBoundsFromGeometry_empty_point", 26 | bounds: mustNewGeometryFromWKT(t, "POINT EMPTY").Bounds(), 27 | expectedEmpty: true, 28 | expectedGeomWKT: "POINT EMPTY", 29 | }, 30 | { 31 | name: "NewBoundsFromGeometry_point", 32 | bounds: mustNewGeometryFromWKT(t, "POINT (0 1)").Bounds(), 33 | expectedEmpty: false, 34 | expectedGeomWKT: "POINT (0 1)", 35 | }, 36 | { 37 | name: "NewBoundsFromGeometry_line_string", 38 | bounds: mustNewGeometryFromWKT(t, "LINESTRING (0 1, 2 3)").Bounds(), 39 | expectedEmpty: false, 40 | expectedGeomWKT: "POLYGON ((0 1, 2 1, 2 3, 0 3, 0 1))", 41 | }, 42 | { 43 | name: "NewBoundsFromGeometry_line_string_empty", 44 | bounds: mustNewGeometryFromWKT(t, "LINESTRING EMPTY").Bounds(), 45 | expectedEmpty: true, 46 | expectedGeomWKT: "POINT EMPTY", 47 | }, 48 | { 49 | name: "NewBoundsFromGeometry_polygon_empty", 50 | bounds: mustNewGeometryFromWKT(t, "POLYGON EMPTY").Bounds(), 51 | expectedEmpty: true, 52 | expectedGeomWKT: "POINT EMPTY", 53 | }, 54 | } { 55 | t.Run(tc.name, func(t *testing.T) { 56 | //nolint:gocritic 57 | assert.True(t, tc.bounds.Equals(tc.bounds)) 58 | assert.Equal(t, tc.expectedEmpty, tc.bounds.IsEmpty()) 59 | expectedGeom, err := geos.NewGeomFromWKT(tc.expectedGeomWKT) 60 | assert.NoError(t, err) 61 | assert.True(t, expectedGeom.Equals(tc.bounds.Geom())) 62 | }) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /geometry/geojson.go: -------------------------------------------------------------------------------- 1 | package geometry 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/twpayne/go-geos" 11 | ) 12 | 13 | var ( 14 | geojsonType = map[geos.TypeID]string{ 15 | geos.TypeIDPoint: "Point", 16 | geos.TypeIDLineString: "LineString", 17 | geos.TypeIDPolygon: "Polygon", 18 | geos.TypeIDMultiPoint: "MultiPoint", 19 | geos.TypeIDMultiLineString: "MultiLineString", 20 | geos.TypeIDMultiPolygon: "MultiPolygon", 21 | geos.TypeIDGeometryCollection: "GeometryCollection", 22 | } 23 | 24 | errUnsupportedEmptyGeometry = errors.New("unsupported empty geometry") 25 | ) 26 | 27 | // NewGeometryFromGeoJSON returns a new Geometry parsed from GeoJSON. 28 | func NewGeometryFromGeoJSON(geoJSON []byte) (*Geometry, error) { 29 | g := &Geometry{} 30 | if err := g.UnmarshalJSON(geoJSON); err != nil { 31 | return nil, err 32 | } 33 | return g, nil 34 | } 35 | 36 | // AsGeoJSON returns the GeoJSON representation of g. 37 | func (g *Geometry) AsGeoJSON() ([]byte, error) { 38 | return g.MarshalJSON() 39 | } 40 | 41 | // MarshalJSON implements encoding/json.Marshaler. 42 | func (g *Geometry) MarshalJSON() ([]byte, error) { 43 | sb := &strings.Builder{} 44 | sb.Grow(initialStringBufferSize) 45 | if err := geojsonWriteGeom(sb, g.Geom); err != nil { 46 | return nil, err 47 | } 48 | return []byte(sb.String()), nil 49 | } 50 | 51 | // UnmarshalJSON implements encoding/json.Unmarshaler. 52 | func (g *Geometry) UnmarshalJSON(data []byte) error { 53 | var geoJSON struct { 54 | Type string `json:"type"` 55 | Coordinates json.RawMessage `json:"coordinates"` 56 | Geometries []json.RawMessage `json:"geometries"` 57 | } 58 | if err := json.Unmarshal(data, &geoJSON); err != nil { 59 | return err 60 | } 61 | switch geoJSON.Type { 62 | case "Point": 63 | var coordinates []float64 64 | if err := json.Unmarshal(geoJSON.Coordinates, &coordinates); err != nil { 65 | return err 66 | } 67 | g.Geom = geos.NewPoint(coordinates) 68 | return nil 69 | case "LineString": 70 | var coordinates [][]float64 71 | if err := json.Unmarshal(geoJSON.Coordinates, &coordinates); err != nil { 72 | return err 73 | } 74 | g.Geom = geos.NewLineString(coordinates) 75 | return nil 76 | case "Polygon": 77 | var coordinates [][][]float64 78 | if err := json.Unmarshal(geoJSON.Coordinates, &coordinates); err != nil { 79 | return err 80 | } 81 | g.Geom = geos.NewPolygon(coordinates) 82 | return nil 83 | case "MultiPoint": 84 | var coordinates [][]float64 85 | if err := json.Unmarshal(geoJSON.Coordinates, &coordinates); err != nil { 86 | return err 87 | } 88 | geoms := make([]*geos.Geom, len(coordinates)) 89 | for i, pointCoord := range coordinates { 90 | geoms[i] = geos.NewPoint(pointCoord) 91 | } 92 | g.Geom = geos.NewCollection(geos.TypeIDMultiPoint, geoms) 93 | return nil 94 | case "MultiLineString": 95 | var coordinates [][][]float64 96 | if err := json.Unmarshal(geoJSON.Coordinates, &coordinates); err != nil { 97 | return err 98 | } 99 | geoms := make([]*geos.Geom, len(coordinates)) 100 | for i, lineStringCoords := range coordinates { 101 | geoms[i] = geos.NewLineString(lineStringCoords) 102 | } 103 | g.Geom = geos.NewCollection(geos.TypeIDMultiLineString, geoms) 104 | return nil 105 | case "MultiPolygon": 106 | var coordinates [][][][]float64 107 | if err := json.Unmarshal(geoJSON.Coordinates, &coordinates); err != nil { 108 | return err 109 | } 110 | geoms := make([]*geos.Geom, len(coordinates)) 111 | for i, polygonCoords := range coordinates { 112 | geoms[i] = geos.NewPolygon(polygonCoords) 113 | } 114 | g.Geom = geos.NewCollection(geos.TypeIDMultiPolygon, geoms) 115 | return nil 116 | case "MultiGeometry": 117 | fallthrough // FIXME handle MultiGeometry 118 | default: 119 | return fmt.Errorf("unsupported type: %s", geoJSON.Type) 120 | } 121 | } 122 | 123 | func geojsonWriteCoordinates(sb *strings.Builder, geom *geos.Geom) error { 124 | for i, coord := range geom.CoordSeq().ToCoords() { 125 | if i != 0 { 126 | if err := sb.WriteByte(','); err != nil { 127 | return err 128 | } 129 | } 130 | if err := sb.WriteByte('['); err != nil { 131 | return err 132 | } 133 | for j, ord := range coord { 134 | if j != 0 { 135 | if err := sb.WriteByte(','); err != nil { 136 | return err 137 | } 138 | } 139 | if _, err := sb.WriteString(strconv.FormatFloat(ord, 'f', -1, 64)); err != nil { 140 | return err 141 | } 142 | } 143 | if err := sb.WriteByte(']'); err != nil { 144 | return err 145 | } 146 | } 147 | return nil 148 | } 149 | 150 | func geojsonWriteCoordinatesArray(sb *strings.Builder, geom *geos.Geom) error { 151 | if err := sb.WriteByte('['); err != nil { 152 | return err 153 | } 154 | if err := geojsonWriteCoordinates(sb, geom); err != nil { 155 | return err 156 | } 157 | return sb.WriteByte(']') 158 | } 159 | 160 | func geojsonWriteGeom(sb *strings.Builder, geom *geos.Geom) error { 161 | if geom == nil { 162 | _, err := sb.WriteString("null") 163 | return err 164 | } 165 | typ, ok := geojsonType[geom.TypeID()] 166 | if !ok { 167 | return fmt.Errorf("unsupported type: %s", geom.Type()) 168 | } 169 | if _, err := sb.WriteString(`{"type":"` + typ + `"`); err != nil { 170 | return err 171 | } 172 | //nolint:exhaustive 173 | switch geom.TypeID() { 174 | case geos.TypeIDPoint: 175 | if geom.IsEmpty() { 176 | return errUnsupportedEmptyGeometry 177 | } 178 | if _, err := sb.WriteString(`,"coordinates":`); err != nil { 179 | return err 180 | } 181 | if err := geojsonWriteCoordinates(sb, geom); err != nil { 182 | return err 183 | } 184 | case geos.TypeIDLineString: 185 | if geom.IsEmpty() { 186 | return errUnsupportedEmptyGeometry 187 | } 188 | if _, err := sb.WriteString(`,"coordinates":`); err != nil { 189 | return err 190 | } 191 | if err := geojsonWriteCoordinatesArray(sb, geom); err != nil { 192 | return err 193 | } 194 | case geos.TypeIDPolygon: 195 | if geom.IsEmpty() { 196 | return errUnsupportedEmptyGeometry 197 | } 198 | if _, err := sb.WriteString(`,"coordinates":`); err != nil { 199 | return err 200 | } 201 | if err := geojsonWritePolygonCoordinates(sb, geom); err != nil { 202 | return err 203 | } 204 | case geos.TypeIDMultiPoint: 205 | if _, err := sb.WriteString(`,"coordinates":[`); err != nil { 206 | return err 207 | } 208 | for i, n := 0, geom.NumGeometries(); i < n; i++ { 209 | if i != 0 { 210 | if err := sb.WriteByte(','); err != nil { 211 | return err 212 | } 213 | } 214 | if err := geojsonWriteCoordinates(sb, geom.Geometry(i)); err != nil { 215 | return err 216 | } 217 | } 218 | if err := sb.WriteByte(']'); err != nil { 219 | return err 220 | } 221 | case geos.TypeIDMultiLineString: 222 | if _, err := sb.WriteString(`,"coordinates":[`); err != nil { 223 | return err 224 | } 225 | for i, n := 0, geom.NumGeometries(); i < n; i++ { 226 | if i != 0 { 227 | if err := sb.WriteByte(','); err != nil { 228 | return err 229 | } 230 | } 231 | if err := geojsonWriteCoordinatesArray(sb, geom.Geometry(i)); err != nil { 232 | return err 233 | } 234 | } 235 | if err := sb.WriteByte(']'); err != nil { 236 | return err 237 | } 238 | case geos.TypeIDMultiPolygon: 239 | if _, err := sb.WriteString(`,"coordinates":[`); err != nil { 240 | return err 241 | } 242 | for i, n := 0, geom.NumGeometries(); i < n; i++ { 243 | if i != 0 { 244 | if err := sb.WriteByte(','); err != nil { 245 | return err 246 | } 247 | } 248 | if err := geojsonWritePolygonCoordinates(sb, geom.Geometry(i)); err != nil { 249 | return err 250 | } 251 | } 252 | if err := sb.WriteByte(']'); err != nil { 253 | return err 254 | } 255 | case geos.TypeIDGeometryCollection: 256 | if _, err := sb.WriteString(`,"geometries":[`); err != nil { 257 | return err 258 | } 259 | for i, n := 0, geom.NumGeometries(); i < n; i++ { 260 | if err := geojsonWriteGeom(sb, geom.Geometry(i)); err != nil { 261 | return err 262 | } 263 | } 264 | if err := sb.WriteByte(']'); err != nil { 265 | return err 266 | } 267 | } 268 | return sb.WriteByte('}') 269 | } 270 | 271 | func geojsonWritePolygonCoordinates(sb *strings.Builder, geom *geos.Geom) error { 272 | if err := sb.WriteByte('['); err != nil { 273 | return err 274 | } 275 | if err := geojsonWriteCoordinatesArray(sb, geom.ExteriorRing()); err != nil { 276 | return err 277 | } 278 | for i, n := 0, geom.NumInteriorRings(); i < n; i++ { 279 | if err := sb.WriteByte(','); err != nil { 280 | return err 281 | } 282 | if err := geojsonWriteCoordinatesArray(sb, geom.InteriorRing(i)); err != nil { 283 | return err 284 | } 285 | } 286 | return sb.WriteByte(']') 287 | } 288 | -------------------------------------------------------------------------------- /geometry/geojson_test.go: -------------------------------------------------------------------------------- 1 | package geometry_test 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | 10 | "github.com/twpayne/go-geos" 11 | "github.com/twpayne/go-geos/geometry" 12 | ) 13 | 14 | var ( 15 | _ json.Marshaler = &geometry.Geometry{} 16 | _ json.Unmarshaler = &geometry.Geometry{} 17 | ) 18 | 19 | func TestGeoJSON(t *testing.T) { 20 | for i, tc := range []struct { 21 | geom *geometry.Geometry 22 | geoJSONStr string 23 | }{ 24 | { 25 | geom: geometry.NewGeometry(geos.NewPoint([]float64{1, 2})), 26 | geoJSONStr: `{"type":"Point","coordinates":[1,2]}`, 27 | }, 28 | } { 29 | t.Run(strconv.Itoa(i), func(t *testing.T) { 30 | actualGeoJSON, err := tc.geom.MarshalJSON() 31 | assert.NoError(t, err) 32 | assert.Equal(t, tc.geoJSONStr, string(actualGeoJSON)) 33 | 34 | var geom geometry.Geometry 35 | assert.NoError(t, geom.UnmarshalJSON([]byte(tc.geoJSONStr))) 36 | assert.True(t, tc.geom.Equals(geom.Geom)) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /geometry/geometry.go: -------------------------------------------------------------------------------- 1 | // Package geometry provides a GEOS-backed geometry type. 2 | package geometry 3 | 4 | import geos "github.com/twpayne/go-geos" 5 | 6 | // initialStringBufferSize is the initial size of strings.Buffers used for 7 | // building GeoJSON and KML representations. 8 | const initialStringBufferSize = 1024 9 | 10 | // A Geometry is a geometry. 11 | type Geometry struct { 12 | *geos.Geom 13 | } 14 | 15 | // Must panics with err if err is non-nil, otherwise it returns g. 16 | func Must(g *Geometry, err error) *Geometry { 17 | if err != nil { 18 | panic(err) 19 | } 20 | return g 21 | } 22 | 23 | // NewGeometry returns a new Geometry using geom. 24 | func NewGeometry(geom *geos.Geom) *Geometry { 25 | return &Geometry{Geom: geom} 26 | } 27 | 28 | // Bounds returns g's bounds. 29 | func (g *Geometry) Bounds() *geos.Box2D { 30 | return g.Geom.Bounds() 31 | } 32 | 33 | // Destroy destroys g's geom. 34 | func (g *Geometry) Destroy() { 35 | if g == nil || g.Geom == nil { 36 | return 37 | } 38 | g.Geom.Destroy() 39 | g.Geom = nil 40 | } 41 | 42 | // SetSRID sets g's SRID. 43 | func (g *Geometry) SetSRID(srid int) *Geometry { 44 | g.Geom.SetSRID(srid) 45 | return g 46 | } 47 | -------------------------------------------------------------------------------- /geometry/geometry_test.go: -------------------------------------------------------------------------------- 1 | package geometry_test 2 | 3 | import ( 4 | "bytes" 5 | "database/sql" 6 | "database/sql/driver" 7 | "encoding/gob" 8 | "encoding/hex" 9 | "encoding/json" 10 | "encoding/xml" 11 | "runtime" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/alecthomas/assert/v2" 16 | 17 | "github.com/twpayne/go-geos" 18 | "github.com/twpayne/go-geos/geometry" 19 | ) 20 | 21 | var ( 22 | _ driver.Value = &geometry.Geometry{} 23 | _ gob.GobEncoder = &geometry.Geometry{} 24 | _ gob.GobDecoder = &geometry.Geometry{} 25 | _ json.Marshaler = &geometry.Geometry{} 26 | _ json.Unmarshaler = &geometry.Geometry{} 27 | _ sql.Scanner = &geometry.Geometry{} 28 | _ xml.Marshaler = &geometry.Geometry{} 29 | ) 30 | 31 | func TestGeometry(t *testing.T) { 32 | for _, tc := range []struct { 33 | name string 34 | geometry *geometry.Geometry 35 | skipGeoJSON bool 36 | expectedGeoJSONError bool 37 | expectedKML string 38 | }{ 39 | { 40 | name: "point_empty", 41 | geometry: mustNewGeometryFromWKT(t, "POINT EMPTY"), 42 | expectedGeoJSONError: true, 43 | }, 44 | { 45 | name: "point", 46 | geometry: mustNewGeometryFromWKT(t, "POINT (0 1)"), 47 | expectedKML: "0,1", 48 | }, 49 | { 50 | name: "linestring_empty", 51 | geometry: mustNewGeometryFromWKT(t, "LINESTRING EMPTY"), 52 | expectedGeoJSONError: true, 53 | expectedKML: "", 54 | }, 55 | { 56 | name: "linestring", 57 | geometry: mustNewGeometryFromWKT(t, "LINESTRING (0 1, 2 3)"), 58 | expectedKML: "0,1 2,3", 59 | }, 60 | { 61 | name: "linearring_empty", 62 | geometry: mustNewGeometryFromWKT(t, "LINEARRING EMPTY"), 63 | expectedGeoJSONError: true, 64 | expectedKML: "", 65 | }, 66 | { 67 | name: "linearring", 68 | geometry: mustNewGeometryFromWKT(t, "LINEARRING (0 0, 1 0, 1 1, 0 0)"), 69 | expectedGeoJSONError: true, 70 | expectedKML: "0,0 1,0 1,1 0,0", 71 | }, 72 | { 73 | name: "polygon_empty", 74 | geometry: mustNewGeometryFromWKT(t, "POLYGON EMPTY"), 75 | expectedGeoJSONError: true, 76 | expectedKML: "", 77 | }, 78 | { 79 | name: "polygon", 80 | geometry: mustNewGeometryFromWKT(t, "POLYGON ((0 0, 1 0, 1 1, 0 0))"), 81 | expectedKML: "0,0 1,0 1,1 0,0", 82 | }, 83 | { 84 | name: "polygon_interior_rings", 85 | geometry: mustNewGeometryFromWKT(t, "POLYGON ((0 0, 3 0, 3 3, 0 3, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1))"), 86 | expectedKML: "" + 87 | "" + 88 | "0,0 3,0 3,3 0,3 0,0" + 89 | "1,1 1,2 2,2 2,1 1,1" + 90 | "", 91 | }, 92 | { 93 | name: "multipoint_empty", 94 | geometry: mustNewGeometryFromWKT(t, "MULTIPOINT EMPTY"), 95 | skipGeoJSON: true, // FIXME re-enable 96 | expectedKML: "", 97 | }, 98 | { 99 | name: "multipoint", 100 | geometry: mustNewGeometryFromWKT(t, "MULTIPOINT (0 1, 2 3)"), 101 | expectedKML: "" + 102 | "" + 103 | "0,1" + 104 | "2,3" + 105 | "", 106 | }, 107 | { 108 | name: "multilinestring_empty", 109 | geometry: mustNewGeometryFromWKT(t, "MULTILINESTRING EMPTY"), 110 | expectedKML: "", 111 | }, 112 | { 113 | name: "multilinestring", 114 | geometry: mustNewGeometryFromWKT(t, "MULTILINESTRING ((0 1, 2 3), (4 5, 6 7))"), 115 | expectedKML: "" + 116 | "" + 117 | "0,1 2,3" + 118 | "4,5 6,7" + 119 | "", 120 | }, 121 | { 122 | name: "multipolygon_empty", 123 | geometry: mustNewGeometryFromWKT(t, "MULTIPOLYGON EMPTY"), 124 | expectedKML: "", 125 | }, 126 | { 127 | name: "multipolygon", 128 | geometry: mustNewGeometryFromWKT(t, "MULTIPOLYGON (((-1 -1, 0 -1, 0 0, -1 -1)), ((0 0, 3 0, 3 3, 0 3, 0 0), (1 1, 1 2, 2 2, 2 1, 1 1)))"), 129 | expectedKML: "" + 130 | "" + 131 | "" + 132 | "-1,-1 0,-1 0,0 -1,-1" + 133 | "" + 134 | "" + 135 | "0,0 3,0 3,3 0,3 0,0" + 136 | "1,1 1,2 2,2 2,1 1,1" + 137 | "" + 138 | "", 139 | }, 140 | { 141 | name: "geometrycollection_empty", 142 | geometry: mustNewGeometryFromWKT(t, "GEOMETRYCOLLECTION EMPTY"), 143 | skipGeoJSON: true, // FIXME re-enable 144 | expectedKML: "", 145 | }, 146 | // FIXME geometrycollection 147 | } { 148 | t.Run(tc.name, func(t *testing.T) { 149 | t.Run("gob", func(t *testing.T) { 150 | defer runtime.GC() // Exercise finalizers. 151 | data := &bytes.Buffer{} 152 | assert.NoError(t, gob.NewEncoder(data).Encode(tc.geometry)) 153 | var actualG geometry.Geometry 154 | assert.NoError(t, gob.NewDecoder(data).Decode(&actualG)) 155 | assert.True(t, actualG.Equals(tc.geometry.Geom)) 156 | }) 157 | t.Run("geojson", func(t *testing.T) { 158 | defer runtime.GC() // Exercise finalizers. 159 | if tc.skipGeoJSON { 160 | t.Skip() 161 | } 162 | geoJSON, err := tc.geometry.AsGeoJSON() 163 | if tc.expectedGeoJSONError { 164 | assert.Error(t, err) 165 | } else { 166 | assert.NoError(t, err) 167 | actualG, err := geometry.NewGeometryFromGeoJSON(geoJSON) 168 | assert.NoError(t, err) 169 | assert.True(t, actualG.Equals(tc.geometry.Geom)) 170 | } 171 | }) 172 | if tc.expectedKML != "" { 173 | t.Run("kml", func(t *testing.T) { 174 | defer runtime.GC() // Exercise finalizers. 175 | data := &strings.Builder{} 176 | assert.NoError(t, xml.NewEncoder(data).Encode(tc.geometry)) 177 | assert.Equal(t, tc.expectedKML, data.String()) 178 | }) 179 | } 180 | t.Run("sql", func(t *testing.T) { 181 | defer runtime.GC() // Exercise finalizers. 182 | value, err := tc.geometry.Value() 183 | assert.NoError(t, err) 184 | var actualG geometry.Geometry 185 | assert.NoError(t, actualG.Scan(value)) 186 | assert.True(t, actualG.Equals(tc.geometry.Geom)) 187 | }) 188 | }) 189 | } 190 | } 191 | 192 | func TestNewGeometry(t *testing.T) { 193 | expected := geometry.NewGeometry(geos.NewPoint([]float64{1, 2})) 194 | 195 | actual, err := geometry.NewGeometryFromGeoJSON([]byte(`{"type":"Point","coordinates":[1,2]}`)) 196 | assert.NoError(t, err) 197 | assert.Equal(t, expected, actual, assert.Exclude[*geos.Context]()) 198 | 199 | wkb, err := hex.DecodeString("0101000000000000000000f03f0000000000000040") 200 | assert.NoError(t, err) 201 | actual, err = geometry.NewGeometryFromWKB(wkb) 202 | assert.NoError(t, err) 203 | assert.Equal(t, expected, actual) 204 | 205 | actual, err = geometry.NewGeometryFromWKT("POINT (1 2)") 206 | assert.NoError(t, err) 207 | assert.Equal(t, expected, actual) 208 | } 209 | -------------------------------------------------------------------------------- /geometry/gob.go: -------------------------------------------------------------------------------- 1 | package geometry 2 | 3 | import "github.com/twpayne/go-geos" 4 | 5 | // GobDecode implements encoding/gob.GobDecoder. 6 | func (g *Geometry) GobDecode(data []byte) error { 7 | if len(data) == 0 { 8 | g.Geom = geos.NewEmptyPoint() 9 | return nil 10 | } 11 | var err error 12 | g.Geom, err = geos.NewGeomFromWKB(data) 13 | return err 14 | } 15 | 16 | // GobEncode implements encoding/gob.GobEncoder. 17 | func (g *Geometry) GobEncode() ([]byte, error) { 18 | if g.Geom == nil { 19 | return nil, nil 20 | } 21 | return g.ToEWKBWithSRID(), nil 22 | } 23 | -------------------------------------------------------------------------------- /geometry/gob_test.go: -------------------------------------------------------------------------------- 1 | package geometry_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | 8 | "github.com/twpayne/go-geos" 9 | "github.com/twpayne/go-geos/geometry" 10 | ) 11 | 12 | func TestGob(t *testing.T) { 13 | g := geometry.NewGeometry(geos.NewPoint([]float64{1, 2})) 14 | data, err := g.GobEncode() 15 | assert.NoError(t, err) 16 | var geom geometry.Geometry 17 | assert.NoError(t, geom.GobDecode(data)) 18 | assert.True(t, g.Equals(geom.Geom)) 19 | } 20 | -------------------------------------------------------------------------------- /geometry/kml.go: -------------------------------------------------------------------------------- 1 | package geometry 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/twpayne/go-geos" 10 | ) 11 | 12 | var ( 13 | kmlPointStartElement = xml.StartElement{Name: xml.Name{Local: "Point"}} 14 | kmlLineStringStartElement = xml.StartElement{Name: xml.Name{Local: "LineString"}} 15 | kmlLinearRingStartElement = xml.StartElement{Name: xml.Name{Local: "LinearRing"}} 16 | kmlPolygonStartElement = xml.StartElement{Name: xml.Name{Local: "Polygon"}} 17 | kmlMultiGeometryStartElement = xml.StartElement{Name: xml.Name{Local: "MultiGeometry"}} 18 | kmlCoordinatesStartElement = xml.StartElement{Name: xml.Name{Local: "coordinates"}} 19 | kmlInnerBoundaryIsStartElement = xml.StartElement{Name: xml.Name{Local: "innerBoundaryIs"}} 20 | kmlOuterBoundaryIsStartElement = xml.StartElement{Name: xml.Name{Local: "outerBoundaryIs"}} 21 | ) 22 | 23 | // MarshalXML implements encoding/xml.Marshaler. 24 | func (g *Geometry) MarshalXML(e *xml.Encoder, _ xml.StartElement) error { 25 | return kmlEncodeGeom(e, g.Geom) 26 | } 27 | 28 | func kmlEncodeCoords(e *xml.Encoder, startElement xml.StartElement, geom *geos.Geom) error { 29 | if err := e.EncodeToken(startElement); err != nil { 30 | return err 31 | } 32 | if coords := geom.CoordSeq().ToCoords(); coords != nil { 33 | if err := e.EncodeToken(kmlCoordinatesStartElement); err != nil { 34 | return err 35 | } 36 | sb := &strings.Builder{} 37 | sb.Grow(initialStringBufferSize) 38 | for i, coord := range coords { 39 | if i != 0 { 40 | if err := sb.WriteByte(' '); err != nil { 41 | return err 42 | } 43 | } 44 | for j, ord := range coord { 45 | if j != 0 { 46 | if err := sb.WriteByte(','); err != nil { 47 | return err 48 | } 49 | } 50 | if _, err := sb.WriteString(strconv.FormatFloat(ord, 'f', -1, 64)); err != nil { 51 | return err 52 | } 53 | } 54 | } 55 | if err := e.EncodeToken(xml.CharData(sb.String())); err != nil { 56 | return err 57 | } 58 | if err := e.EncodeToken(kmlCoordinatesStartElement.End()); err != nil { 59 | return err 60 | } 61 | } 62 | return e.EncodeToken(startElement.End()) 63 | } 64 | 65 | func kmlEncodeGeom(e *xml.Encoder, geom *geos.Geom) error { 66 | switch geom.TypeID() { 67 | case geos.TypeIDPoint: 68 | return kmlEncodeCoords(e, kmlPointStartElement, geom) 69 | case geos.TypeIDLineString: 70 | return kmlEncodeCoords(e, kmlLineStringStartElement, geom) 71 | case geos.TypeIDLinearRing: 72 | return kmlEncodeCoords(e, kmlLinearRingStartElement, geom) 73 | case geos.TypeIDPolygon: 74 | return kmlEncodePolygon(e, geom) 75 | case geos.TypeIDMultiPoint: 76 | fallthrough 77 | case geos.TypeIDMultiLineString: 78 | fallthrough 79 | case geos.TypeIDMultiPolygon: 80 | fallthrough 81 | case geos.TypeIDGeometryCollection: 82 | return kmlEncodeMultiGeometry(e, geom) 83 | default: 84 | return fmt.Errorf("unsupported type: %s", geom.Type()) 85 | } 86 | } 87 | 88 | func kmlEncodeLinearRing(e *xml.Encoder, startElement xml.StartElement, geom *geos.Geom) error { 89 | if err := e.EncodeToken(startElement); err != nil { 90 | return err 91 | } 92 | if err := kmlEncodeCoords(e, kmlLinearRingStartElement, geom); err != nil { 93 | return err 94 | } 95 | return e.EncodeToken(startElement.End()) 96 | } 97 | 98 | func kmlEncodeMultiGeometry(e *xml.Encoder, geom *geos.Geom) error { 99 | if err := e.EncodeToken(kmlMultiGeometryStartElement); err != nil { 100 | return err 101 | } 102 | for i, n := 0, geom.NumGeometries(); i < n; i++ { 103 | if err := kmlEncodeGeom(e, geom.Geometry(i)); err != nil { 104 | return err 105 | } 106 | } 107 | return e.EncodeToken(kmlMultiGeometryStartElement.End()) 108 | } 109 | 110 | func kmlEncodePolygon(e *xml.Encoder, geom *geos.Geom) error { 111 | if err := e.EncodeToken(kmlPolygonStartElement); err != nil { 112 | return err 113 | } 114 | if !geom.IsEmpty() { 115 | if err := kmlEncodeLinearRing(e, kmlOuterBoundaryIsStartElement, geom.ExteriorRing()); err != nil { 116 | return err 117 | } 118 | for i, n := 0, geom.NumInteriorRings(); i < n; i++ { 119 | if err := kmlEncodeLinearRing(e, kmlInnerBoundaryIsStartElement, geom.InteriorRing(i)); err != nil { 120 | return err 121 | } 122 | } 123 | } 124 | return e.EncodeToken(kmlPolygonStartElement.End()) 125 | } 126 | -------------------------------------------------------------------------------- /geometry/kml_test.go: -------------------------------------------------------------------------------- 1 | package geometry_test 2 | 3 | import ( 4 | "encoding/xml" 5 | 6 | "github.com/twpayne/go-geos/geometry" 7 | ) 8 | 9 | var _ xml.Marshaler = &geometry.Geometry{} 10 | -------------------------------------------------------------------------------- /geometry/sql.go: -------------------------------------------------------------------------------- 1 | package geometry 2 | 3 | import ( 4 | "database/sql/driver" 5 | "encoding/hex" 6 | "fmt" 7 | 8 | "github.com/twpayne/go-geos" 9 | ) 10 | 11 | // Scan implements database/sql.Scanner. 12 | func (g *Geometry) Scan(src any) error { 13 | switch src := src.(type) { 14 | case nil: 15 | g.Geom = nil 16 | return nil 17 | case []byte: 18 | return g.scanWKB(src) 19 | case string: 20 | wkb, err := hex.DecodeString(src) 21 | if err != nil { 22 | return err 23 | } 24 | return g.scanWKB(wkb) 25 | default: 26 | return fmt.Errorf("want nil, []byte, or string, got %T", src) 27 | } 28 | } 29 | 30 | func (g *Geometry) scanWKB(wkb []byte) error { 31 | if len(wkb) == 0 { 32 | g.Geom = geos.NewEmptyPoint() 33 | return nil 34 | } 35 | geom, err := geos.NewGeomFromWKB(wkb) 36 | if err != nil { 37 | return err 38 | } 39 | g.Geom = geom 40 | return nil 41 | } 42 | 43 | // Value implements database/sql/driver.Value. 44 | func (g Geometry) Value() (driver.Value, error) { 45 | if g.Geom == nil { 46 | return nil, nil //nolint:nilnil 47 | } 48 | return hex.EncodeToString(g.ToEWKBWithSRID()), nil 49 | } 50 | -------------------------------------------------------------------------------- /geometry/sql_test.go: -------------------------------------------------------------------------------- 1 | package geometry_test 2 | 3 | import ( 4 | "database/sql" 5 | "database/sql/driver" 6 | 7 | "github.com/twpayne/go-geos/geometry" 8 | ) 9 | 10 | var ( 11 | _ driver.Value = &geometry.Geometry{} 12 | _ sql.Scanner = &geometry.Geometry{} 13 | ) 14 | -------------------------------------------------------------------------------- /geometry/text.go: -------------------------------------------------------------------------------- 1 | package geometry 2 | 3 | import "github.com/twpayne/go-geos" 4 | 5 | // NewGeometryFromWKT returns a new Geometry from wkt. 6 | func NewGeometryFromWKT(wkt string) (*Geometry, error) { 7 | geom, err := geos.NewGeomFromWKT(wkt) 8 | if err != nil { 9 | return nil, err 10 | } 11 | return &Geometry{Geom: geom}, nil 12 | } 13 | 14 | // MarshalText implements encoding.TextMarshaler. 15 | func (g *Geometry) MarshalText() ([]byte, error) { 16 | return []byte(g.ToWKT()), nil 17 | } 18 | 19 | // UnmarshalText implements encoding.TextUnmarshaler. 20 | func (g *Geometry) UnmarshalText(data []byte) error { 21 | geom, err := geos.NewGeomFromWKT(string(data)) 22 | if err != nil { 23 | return err 24 | } 25 | g.Geom = geom 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /geometry/text_test.go: -------------------------------------------------------------------------------- 1 | package geometry_test 2 | 3 | import ( 4 | "encoding" 5 | "strconv" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | 10 | "github.com/twpayne/go-geos" 11 | "github.com/twpayne/go-geos/geometry" 12 | ) 13 | 14 | var ( 15 | _ encoding.TextMarshaler = &geometry.Geometry{} 16 | _ encoding.TextUnmarshaler = &geometry.Geometry{} 17 | ) 18 | 19 | func TestText(t *testing.T) { 20 | for i, tc := range []struct { 21 | geom *geometry.Geometry 22 | textStr string 23 | textPre3_12Str string 24 | }{ 25 | { 26 | geom: geometry.NewGeometry(geos.NewPoint([]float64{1, 2})), 27 | textStr: "POINT (1 2)", 28 | textPre3_12Str: "POINT (1.0000000000000000 2.0000000000000000)", 29 | }, 30 | } { 31 | t.Run(strconv.Itoa(i), func(t *testing.T) { 32 | textStr := tc.textStr 33 | if geos.VersionCompare(3, 12, 0) < 0 { 34 | textStr = tc.textPre3_12Str 35 | } 36 | text, err := tc.geom.MarshalText() 37 | assert.NoError(t, err) 38 | assert.Equal(t, textStr, string(text)) 39 | 40 | var geom geometry.Geometry 41 | assert.NoError(t, geom.UnmarshalText([]byte(textStr))) 42 | assert.True(t, tc.geom.Equals(geom.Geom)) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /geometry/util_test.go: -------------------------------------------------------------------------------- 1 | package geometry_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/alecthomas/assert/v2" 7 | 8 | "github.com/twpayne/go-geos" 9 | "github.com/twpayne/go-geos/geometry" 10 | ) 11 | 12 | func mustNewGeometryFromWKT(t *testing.T, wkt string) *geometry.Geometry { 13 | t.Helper() 14 | geom, err := geos.NewGeomFromWKT(wkt) 15 | assert.NoError(t, err) 16 | return &geometry.Geometry{Geom: geom} 17 | } 18 | -------------------------------------------------------------------------------- /geommethods.go.tmpl: -------------------------------------------------------------------------------- 1 | // This file is automatically generated. DO NOT EDIT. 2 | 3 | package geos 4 | 5 | // #include "go-geos.h" 6 | import "C" 7 | 8 | import "unsafe" 9 | 10 | {{- range . }} 11 | {{- $geosFunction := printf "GEOS%s_r" .name }} 12 | {{- if .geosFunction }} 13 | {{- if eq .geosFunction $geosFunction }} 14 | {{- with printf "%s: do not set default .geosFunction" .name }} 15 | {{- fatal . }} 16 | {{- end }} 17 | {{- end }} 18 | {{- $geosFunction = .geosFunction }} 19 | {{- end }} 20 | {{- if not .comment }} 21 | {{ with printf "%s: comment not set" .name }} 22 | {{- fatal . }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{- if eq .type "unary" }} 27 | 28 | {{ if .comment }}// {{ .name }} {{ .comment }}.{{ end }} 29 | func (g *Geom) {{ .name }}({{ range $index, $arg := .extraArgs }}{{ if $index }}, {{ end }}{{ $arg.name }} {{ $arg.type }}{{ end }}) *Geom { 30 | g.mustNotBeDestroyed() 31 | g.context.Lock() 32 | defer g.context.Unlock() 33 | return g.context.new{{ if not .nil }}NonNil{{ end }}Geom(C.{{ $geosFunction }}(g.context.handle, g.geom{{ range .extraArgs }}, {{ .type | cType }}({{ .name }}){{ end }}), nil) 34 | } 35 | 36 | {{- else if eq .type "binary" }} 37 | 38 | {{ if .comment }}// {{ .name }} {{ .comment }}.{{ end }} 39 | func (g *Geom) {{ .name }}(other *Geom{{ range .extraArgs }}, {{ .name }} {{ .type }}{{ end }}) *Geom { 40 | g.mustNotBeDestroyed() 41 | g.context.Lock() 42 | defer g.context.Unlock() 43 | return g.context.newGeom(C.{{ $geosFunction }}(g.context.handle, g.geom, other.geom{{ range .extraArgs }}, {{ .type | cType }}({{ .name }}){{ end }}), nil) 44 | } 45 | 46 | {{- else if eq .type "unaryPredicate" }} 47 | 48 | {{ if .comment }}// {{ .name }} {{ .comment }}.{{ end }} 49 | func (g *Geom) {{ .name }}() bool { 50 | g.mustNotBeDestroyed() 51 | g.context.Lock() 52 | defer g.context.Unlock() 53 | switch C.{{ $geosFunction }}(g.context.handle, g.geom) { 54 | case 0: 55 | return false 56 | case 1: 57 | return true 58 | default: 59 | panic(g.context.err) 60 | } 61 | } 62 | 63 | {{- else if eq .type "binaryPredicate" }} 64 | 65 | {{ if .comment }}// {{ .name }} {{ .comment }}.{{ end }} 66 | func (g *Geom) {{ .name }}(other *Geom{{ range .extraArgs }}, {{ .name }} {{ .type }}{{ end }}) bool { 67 | g.mustNotBeDestroyed() 68 | g.context.Lock() 69 | defer g.context.Unlock() 70 | switch C.{{ $geosFunction }}(g.context.handle, g.geom, other.geom{{ range .extraArgs }}, {{ .type | cType }}({{ .name }}){{ end }}) { 71 | case 0: 72 | return false 73 | case 1: 74 | return true 75 | default: 76 | panic(g.context.err) 77 | } 78 | } 79 | 80 | {{- else if eq .type "float64Property" }} 81 | {{- $varName := .name | firstRuneToLower }} 82 | 83 | {{ if .comment }}// {{ .name }} {{ .comment }}.{{ end }} 84 | func (g *Geom) {{ .name }}() float64 { 85 | g.mustNotBeDestroyed() 86 | g.context.Lock() 87 | defer g.context.Unlock() 88 | var {{ $varName }} float64 89 | if C.{{ $geosFunction }}(g.context.handle, g.geom, (*C.double)(&{{ $varName }})) == 0 { 90 | panic(g.context.err) 91 | } 92 | return {{ $varName }} 93 | } 94 | 95 | {{- else if eq .type "float64BinaryProperty" }} 96 | {{- $varName := .name | firstRuneToLower }} 97 | 98 | {{ if .comment }}// {{ .name }} {{ .comment }}.{{ end }} 99 | func (g *Geom) {{ .name }}(other *Geom{{ range .extraArgs }}, {{ .name }} {{ .type }}{{ end }}) float64 { 100 | g.mustNotBeDestroyed() 101 | g.context.Lock() 102 | defer g.context.Unlock() 103 | {{- if .valueReturned }} 104 | return float64(C.{{ $geosFunction }}(g.context.handle, g.geom, other.geom{{ range .extraArgs }}, {{ .type | cType }}({{ .name }}){{ end }})) 105 | {{- else }} 106 | var {{ $varName }} float64 107 | if C.{{ $geosFunction }}(g.context.handle, g.geom, other.geom{{ range .extraArgs }}, {{ .type | cType }}({{ .name }}){{ end }}, (*C.double)(&{{ $varName }})) == 0 { 108 | panic(g.context.err) 109 | } 110 | return {{ $varName }} 111 | {{- end }} 112 | } 113 | 114 | {{- else if eq .type "stringBinaryProperty" }} 115 | {{- $varName := .name | firstRuneToLower }} 116 | 117 | {{ if .comment }}// {{ .name }} {{ .comment }}.{{ end }} 118 | func (g *Geom) {{ .name }}(other *Geom{{ range .extraArgs }}, {{ .name }} {{ .type }}{{ end }}) string { 119 | g.mustNotBeDestroyed() 120 | g.context.Lock() 121 | defer g.context.Unlock() 122 | {{ $varName }}CStr := C.{{ $geosFunction }}(g.context.handle, g.geom, other.geom{{ range .extraArgs }}, {{ .type | cType }}({{ .name }}){{ end }}) 123 | defer C.GEOSFree_r(g.context.handle, unsafe.Pointer({{ $varName }}CStr)) 124 | return C.GoString({{ $varName }}CStr) 125 | } 126 | 127 | {{- end }} 128 | 129 | {{- end }} 130 | -------------------------------------------------------------------------------- /geommethods.yaml: -------------------------------------------------------------------------------- 1 | - name: Area 2 | comment: returns g's area 3 | type: float64Property 4 | - name: Boundary 5 | comment: returns the boundary of g 6 | type: unary 7 | - name: Buffer 8 | comment: returns g with the given buffer 9 | type: unary 10 | extraArgs: 11 | - name: width 12 | type: float64 13 | - name: quadsegs 14 | type: int 15 | - name: BufferWithStyle 16 | comment: returns a buffer using the provided style parameters 17 | type: unary 18 | extraArgs: 19 | - name: width 20 | type: float64 21 | - name: quadsegs 22 | type: int 23 | - name: endCapStyle 24 | type: BufCapStyle 25 | - name: joinStyle 26 | type: BufJoinStyle 27 | - name: mitreLimit 28 | type: float64 29 | - name: BuildArea 30 | comment: returns the polygonization using all the linework, assuming that rings contained within rings are empty holes, rather than extra PolygonHoleSimplify 31 | type: unary 32 | - name: Centroid 33 | comment: returns a point at the center of mass of g 34 | type: unary 35 | geosFunction: GEOSGetCentroid_r 36 | - name: ClipByRect 37 | comment: returns g clipped to a rectangular polygon 38 | type: unary 39 | extraArgs: 40 | - name: minX 41 | type: float64 42 | - name: minY 43 | type: float64 44 | - name: maxX 45 | type: float64 46 | - name: maxY 47 | type: float64 48 | - name: Clone 49 | comment: returns a clone of g 50 | type: unary 51 | geosFunction: GEOSGeom_clone_r 52 | - name: ConcaveHull 53 | comment: returns the concave hull of g 54 | type: unary 55 | extraArgs: 56 | - name: ratio 57 | type: float64 58 | - name: allowHoles 59 | type: uint 60 | - name: ConcaveHullByLength 61 | comment: returns the concave hull of g 62 | type: unary 63 | extraArgs: 64 | - name: ratio 65 | type: float64 66 | - name: allowHoles 67 | type: uint 68 | - name: ConstrainedDelaunayTriangulation 69 | comment: returns the constrained Delaunay triangulation of the vertices of the g 70 | type: unary 71 | - name: Contains 72 | comment: returns true if g contains other 73 | type: binaryPredicate 74 | - name: ConvexHull 75 | comment: returns g's convex hull 76 | type: unary 77 | - name: CoverageUnion 78 | comment: returns the union of g for polygonal inputs that are correctly noded and do not overlap 79 | type: unary 80 | - name: CoveredBy 81 | comment: returns true if g is covered by other 82 | type: binaryPredicate 83 | - name: Covers 84 | comment: returns true if g covers other 85 | type: binaryPredicate 86 | - name: Crosses 87 | comment: returns true if g crosses other 88 | type: binaryPredicate 89 | - name: Densify 90 | comment: returns g densified with the given tolerance 91 | type: unary 92 | extraArgs: 93 | - name: tolerance 94 | type: float64 95 | - name: Difference 96 | comment: returns the difference between g and other 97 | type: binary 98 | - name: DifferencePrec 99 | comment: returns the difference between g and other 100 | type: binary 101 | extraArgs: 102 | - name: gridSize 103 | type: float64 104 | - name: Disjoint 105 | comment: returns true if g is disjoint from other 106 | type: binaryPredicate 107 | - name: DisjointSubsetUnion 108 | comment: returns the union of all components of a single geometry (optimized for inputs that can be divided into subsets that do not intersect) 109 | type: unary 110 | - name: Distance 111 | comment: returns the distance between the closes points on g and other 112 | type: float64BinaryProperty 113 | - name: DistanceIndexed 114 | comment: returns the distance between g and other, using the indexed facet distance 115 | type: float64BinaryProperty 116 | - name: DistanceWithin 117 | comment: returns whether the distance between g and other is within the given dist 118 | type: binaryPredicate 119 | extraArgs: 120 | - name: dist 121 | type: float64 122 | - name: EndPoint 123 | comment: returns the last point of a LineString 124 | type: unary 125 | geosFunction: GEOSGeomGetEndPoint_r 126 | - name: Envelope 127 | comment: returns the envelope of g 128 | type: unary 129 | - name: Equals 130 | comment: returns true if g equals other 131 | type: binaryPredicate 132 | - name: EqualsExact 133 | comment: returns true if g equals other exactly 134 | type: binaryPredicate 135 | extraArgs: 136 | - name: tolerance 137 | type: float64 138 | - name: FrechetDistance 139 | comment: returns the Fréchet distance between g and other 140 | type: float64BinaryProperty 141 | - name: FrechetDistanceDensify 142 | comment: returns the Fréchet distance between g and other 143 | type: float64BinaryProperty 144 | extraArgs: 145 | - name: densifyFrac 146 | type: float64 147 | - name: HasZ 148 | comment: returns if g has Z coordinates 149 | type: unaryPredicate 150 | - name: HausdorffDistance 151 | comment: returns the Hausdorff distance between g and other 152 | type: float64BinaryProperty 153 | - name: HausdorffDistanceDensify 154 | comment: returns the Hausdorff distance between g and other 155 | type: float64BinaryProperty 156 | extraArgs: 157 | - name: densifyFrac 158 | type: float64 159 | - name: Interpolate 160 | comment: returns a point distance d from the start of g, which must be a linestring 161 | type: unary 162 | nil: true 163 | extraArgs: 164 | - name: d 165 | type: float64 166 | - name: InterpolateNormalized 167 | comment: returns the point that is at proportion from the start 168 | type: unary 169 | nil: true 170 | extraArgs: 171 | - name: proportion 172 | type: float64 173 | - name: Intersection 174 | comment: returns the intersection of g and other 175 | type: binary 176 | - name: IntersectionPrec 177 | comment: returns the intersection of g and other 178 | type: binary 179 | extraArgs: 180 | - name: gridSize 181 | type: float64 182 | - name: Intersects 183 | comment: returns true if g intersects other 184 | type: binaryPredicate 185 | - name: IsClosed 186 | comment: returns true if g is closed 187 | type: unaryPredicate 188 | geosFunction: GEOSisClosed_r 189 | - name: IsEmpty 190 | comment: returns true if g is empty 191 | type: unaryPredicate 192 | geosFunction: GEOSisEmpty_r 193 | - name: IsRing 194 | comment: returns true if g is a ring 195 | type: unaryPredicate 196 | geosFunction: GEOSisRing_r 197 | - name: IsSimple 198 | comment: returns true if g is simple 199 | type: unaryPredicate 200 | geosFunction: GEOSisSimple_r 201 | - name: IsValid 202 | comment: returns true if g is valid 203 | type: unaryPredicate 204 | geosFunction: GEOSisValid_r 205 | - name: LargestEmptyCircle 206 | comment: returns the largest empty circle for g, up to a specified tolerance 207 | type: binary 208 | extraArgs: 209 | - name: tolerance 210 | type: float64 211 | - name: Length 212 | comment: returns g's length 213 | type: float64Property 214 | - name: LineMerge 215 | comment: returns a set of fully noded LineStrings, removing any cardinality 2 nodes in the linework 216 | type: unary 217 | - name: MakeValid 218 | comment: repairs an invalid geometry, returning a valid output 219 | type: unary 220 | - name: MaximumInscribedCircle 221 | comment: returns the maximum inscribed circle of g up to the the given tolerance 222 | type: unary 223 | extraArgs: 224 | - name: tolerance 225 | type: float64 226 | - name: MinimumClearance 227 | comment: returns the minimum clearance of g 228 | type: float64Property 229 | - name: MinimumClearanceLine 230 | comment: returns a LineString whose endpoints define the minimum clearance of g 231 | type: unary 232 | - name: MinimumRotatedRectangle 233 | comment: returns the minimum rotated rectangle enclosing g 234 | type: unary 235 | - name: MinimumWidth 236 | comment: returns a linestring geometry which represents the minimum diameter of g 237 | type: unary 238 | - name: Node 239 | comment: returns a new geometry in which no lines cross each other, and all touching occurs at endpoints 240 | type: unary 241 | - name: OffsetCurve 242 | comment: returns the offset curve line(s) of g 243 | type: unary 244 | extraArgs: 245 | - name: width 246 | type: float64 247 | - name: quadsegs 248 | type: int 249 | #- name: endCapStyle 250 | # type: BufCapStyle 251 | - name: joinStyle 252 | type: BufJoinStyle 253 | - name: mitreLimit 254 | type: float64 255 | - name: Overlaps 256 | comment: returns true if g overlaps other 257 | type: binaryPredicate 258 | #- name: PolygonHoleSimplify 259 | # type: unary 260 | # extraArgs: 261 | # - name: isOuter 262 | # type: int 263 | # - name: vertexNumFraction 264 | # type: float64 265 | #- name: PolygonHoleSimplifyMode 266 | # type: unary 267 | # extraArgs: 268 | # - name: isOuter 269 | # type: int 270 | # - name: parameterMode 271 | # type: uint 272 | # - name: vertexNumFraction 273 | # type: float64 274 | - name: PointOnSurface 275 | comment: returns a point that is inside the boundary of a polygonal geometry 276 | type: unary 277 | - name: Project 278 | comment: returns the distance of other(a point) projected onto g(a line) from the start of the line 279 | type: float64BinaryProperty 280 | valueReturned: true 281 | - name: ProjectNormalized 282 | comment: returns the proportional distance of other(a point) projected onto g(a line) from the start of the line. For example, a point that projects to the middle of a line would be return 0.5 283 | type: float64BinaryProperty 284 | valueReturned: true 285 | - name: Relate 286 | comment: returns the DE9IM pattern for g and other 287 | type: stringBinaryProperty 288 | - name: RelateBoundaryNodeRule 289 | comment: returns the DE9IM pattern for g and other 290 | type: stringBinaryProperty 291 | extraArgs: 292 | - name: bnr 293 | type: RelateBoundaryNodeRule 294 | - name: Reverse 295 | comment: returns g with sequence orders reversed 296 | type: unary 297 | - name: SetPrecision 298 | comment: changes the coordinate precision of g 299 | type: unary 300 | geosFunction: GEOSGeom_setPrecision_r 301 | extraArgs: 302 | - name: gridSize 303 | type: float64 304 | - name: flags 305 | type: PrecisionRule 306 | - name: SharedPaths 307 | comment: returns the paths shared between g and other, which must be lineal geometries 308 | type: binary 309 | - name: Simplify 310 | comment: returns a simplified geometry 311 | type: unary 312 | extraArgs: 313 | - name: tolerance 314 | type: float64 315 | - name: Snap 316 | comment: returns a geometry with the vertices and segments of g snapped to other within the given tolerance 317 | type: binary 318 | extraArgs: 319 | - name: tolerance 320 | type: float64 321 | - name: StartPoint 322 | comment: returns the first point of a LineString 323 | type: unary 324 | geosFunction: GEOSGeomGetStartPoint_r 325 | - name: SymDifference 326 | comment: returns the symmetric difference between g and other 327 | type: binary 328 | - name: SymDifferencePrec 329 | comment: returns the symmetric difference between g and other 330 | type: binary 331 | extraArgs: 332 | - name: gridSize 333 | type: float64 334 | - name: TopologyPreserveSimplify 335 | comment: returns a simplified geometry preserving topology 336 | type: unary 337 | extraArgs: 338 | - name: tolerance 339 | type: float64 340 | - name: Touches 341 | comment: returns true if g touches other 342 | type: binaryPredicate 343 | - name: UnaryUnion 344 | comment: returns the union of all components of a single geometry 345 | type: unary 346 | - name: UnaryUnionPrec 347 | comment: returns the union of all components of a single geometry 348 | type: unary 349 | extraArgs: 350 | - name: gridSize 351 | type: float64 352 | - name: Union 353 | comment: returns the union of g and other 354 | type: binary 355 | - name: UnionPrec 356 | comment: returns the union of g and other 357 | type: binary 358 | extraArgs: 359 | - name: gridSize 360 | type: float64 361 | - name: Within 362 | comment: returns true if g is within other 363 | type: binaryPredicate 364 | - name: X 365 | comment: returns g's X coordinate 366 | type: float64Property 367 | geosFunction: GEOSGeomGetX_r 368 | - name: Y 369 | comment: returns g's Y coordinate 370 | type: float64Property 371 | geosFunction: GEOSGeomGetY_r 372 | -------------------------------------------------------------------------------- /geos.go: -------------------------------------------------------------------------------- 1 | // Package geos provides an interface to GEOS. See https://trac.osgeo.org/geos/. 2 | package geos 3 | 4 | // #cgo pkg-config: geos 5 | // #include "go-geos.h" 6 | import "C" 7 | 8 | // Version. 9 | const ( 10 | VersionMajor = C.GEOS_VERSION_MAJOR 11 | VersionMinor = C.GEOS_VERSION_MINOR 12 | VersionPatch = C.GEOS_VERSION_PATCH 13 | ) 14 | 15 | // A TypeID is a geometry type id. 16 | type TypeID int 17 | 18 | // Geometry type ids. 19 | const ( 20 | TypeIDPoint TypeID = C.GEOS_POINT 21 | TypeIDLineString TypeID = C.GEOS_LINESTRING 22 | TypeIDLinearRing TypeID = C.GEOS_LINEARRING 23 | TypeIDPolygon TypeID = C.GEOS_POLYGON 24 | TypeIDMultiPoint TypeID = C.GEOS_MULTIPOINT 25 | TypeIDMultiLineString TypeID = C.GEOS_MULTILINESTRING 26 | TypeIDMultiPolygon TypeID = C.GEOS_MULTIPOLYGON 27 | TypeIDGeometryCollection TypeID = C.GEOS_GEOMETRYCOLLECTION 28 | ) 29 | 30 | // A RelateBoundaryNodeRule is a relate boundary node rule. 31 | type RelateBoundaryNodeRule int 32 | 33 | // Boundary node rules. 34 | const ( 35 | RelateBoundaryNodeRuleMod2 RelateBoundaryNodeRule = C.GEOSRELATE_BNR_MOD2 36 | RelateBoundaryNodeRuleOGC RelateBoundaryNodeRule = C.GEOSRELATE_BNR_OGC 37 | RelateBoundaryNodeRuleEndpoint RelateBoundaryNodeRule = C.GEOSRELATE_BNR_ENDPOINT 38 | RelateBoundaryNodeRuleMultivalentEndpoint RelateBoundaryNodeRule = C.GEOSRELATE_BNR_MULTIVALENT_ENDPOINT 39 | RelateBoundaryNodeRuleMonovalentEndpoint RelateBoundaryNodeRule = C.GEOSRELATE_BNR_MONOVALENT_ENDPOINT 40 | ) 41 | 42 | type BufCapStyle int 43 | 44 | // Buffer cap styles. 45 | const ( 46 | BufCapStyleRound BufCapStyle = C.GEOSBUF_CAP_ROUND 47 | BufCapStyleFlat BufCapStyle = C.GEOSBUF_CAP_FLAT 48 | BufCapStyleSquare BufCapStyle = C.GEOSBUF_CAP_SQUARE 49 | ) 50 | 51 | type BufJoinStyle int 52 | 53 | // Buffer join styles. 54 | const ( 55 | BufJoinStyleRound BufJoinStyle = C.GEOSBUF_JOIN_ROUND 56 | BufJoinStyleMitre BufJoinStyle = C.GEOSBUF_JOIN_MITRE 57 | BufJoinStyleBevel BufJoinStyle = C.GEOSBUF_JOIN_BEVEL 58 | ) 59 | 60 | // An Error is an error returned by GEOS. 61 | type Error string 62 | 63 | func (e Error) Error() string { 64 | return string(e) 65 | } 66 | 67 | var ( 68 | errContextMismatch = Error("context mismatch") 69 | errDimensionOutOfRange = Error("dimension out of range") 70 | errDuplicateValue = Error("duplicate value") 71 | errIndexOutOfRange = Error("index out of range") 72 | ) 73 | 74 | type PrecisionRule int 75 | 76 | // Precision rules. 77 | const ( 78 | PrecisionRuleNone PrecisionRule = 0 79 | PrecisionRuleValidOutput PrecisionRule = C.GEOS_PREC_VALID_OUTPUT 80 | PrecisionRuleNoTopo PrecisionRule = C.GEOS_PREC_NO_TOPO 81 | PrecisionRulePointwise PrecisionRule = C.GEOS_PREC_NO_TOPO 82 | PrecisionRuleKeepCollapsed PrecisionRule = C.GEOS_PREC_KEEP_COLLAPSED 83 | ) 84 | 85 | type MakeValidMethod int 86 | 87 | // MakeValidMethods. 88 | const ( 89 | MakeValidLinework MakeValidMethod = C.GEOS_MAKE_VALID_LINEWORK 90 | MakeValidStructure MakeValidMethod = C.GEOS_MAKE_VALID_STRUCTURE 91 | ) 92 | 93 | type MakeValidCollapsed int 94 | 95 | // MakeValidMethods. 96 | const ( 97 | MakeValidDiscardCollapsed MakeValidCollapsed = 0 98 | MakeValidKeepCollapsed MakeValidCollapsed = 1 99 | ) 100 | 101 | // VersionCompare returns a negative number if the GEOS version is less than the 102 | // given major.minor.patch version, zero if it is equal, or a positive number 103 | // otherwise. 104 | func VersionCompare(major, minor, patch int) int { 105 | if majorDelta := VersionMajor - major; majorDelta != 0 { 106 | return majorDelta 107 | } 108 | if minorDelta := VersionMinor - minor; minorDelta != 0 { 109 | return minorDelta 110 | } 111 | return VersionPatch - patch 112 | } 113 | -------------------------------------------------------------------------------- /geos_test.go: -------------------------------------------------------------------------------- 1 | package geos_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | 9 | "github.com/twpayne/go-geos" 10 | ) 11 | 12 | func mustNewGeomFromWKT(t *testing.T, c *geos.Context, wkt string) *geos.Geom { 13 | t.Helper() 14 | geom, err := c.NewGeomFromWKT(wkt) 15 | if err != nil { 16 | err = fmt.Errorf("%s: %w", wkt, err) 17 | } 18 | assert.NoError(t, err) 19 | assert.True(t, geom.IsValid()) 20 | return geom 21 | } 22 | 23 | func newInvalidGeomFromWKT(t *testing.T, c *geos.Context, wkt string) *geos.Geom { 24 | t.Helper() 25 | geom, err := c.NewGeomFromWKT(wkt) 26 | if err != nil { 27 | err = fmt.Errorf("%s: %w", wkt, err) 28 | } 29 | assert.NoError(t, err) 30 | assert.False(t, geom.IsValid()) 31 | return geom 32 | } 33 | -------------------------------------------------------------------------------- /go-geos.c: -------------------------------------------------------------------------------- 1 | #include "go-geos.h" 2 | 3 | // Using cgo to call C functions from Go has a high overhead. The functions in 4 | // this file batch multiple calls to GEOS in C (rather than Go) to increase 5 | // performance. 6 | 7 | enum { bounds_MinX, bounds_MinY, bounds_MaxX, bounds_MaxY }; 8 | 9 | uintptr_t c_GEOSGeom_getUserData_r(GEOSContextHandle_t handle, 10 | const GEOSGeometry *g) { 11 | void *userdata = GEOSGeom_getUserData_r(handle, g); 12 | return (uintptr_t)userdata; 13 | } 14 | 15 | void c_GEOSGeom_setUserData_r(GEOSContextHandle_t handle, GEOSGeometry *g, 16 | uintptr_t userdata) { 17 | GEOSGeom_setUserData_r(handle, g, (void *)userdata); 18 | } 19 | 20 | // c_GEOSGeomBounds_r extends bounds to include g. 21 | void c_GEOSGeomBounds_r(GEOSContextHandle_t handle, const GEOSGeometry *g, 22 | double *minX, double *minY, double *maxX, 23 | double *maxY) { 24 | if (GEOSisEmpty_r(handle, g)) { 25 | return; 26 | } 27 | 28 | switch (GEOSGeomTypeId_r(handle, g)) { 29 | case GEOS_POINT: { 30 | double x; 31 | GEOSGeomGetX_r(handle, g, &x); 32 | if (x < *minX) { 33 | *minX = x; 34 | } 35 | if (x > *maxX) { 36 | *maxX = x; 37 | } 38 | double y; 39 | GEOSGeomGetY_r(handle, g, &y); 40 | if (y < *minY) { 41 | *minY = y; 42 | } 43 | if (y > *maxY) { 44 | *maxY = y; 45 | } 46 | } break; 47 | case GEOS_LINESTRING: 48 | // fallthrough 49 | case GEOS_LINEARRING: { 50 | const GEOSCoordSequence *s = GEOSGeom_getCoordSeq_r(handle, g); 51 | unsigned int size; 52 | GEOSCoordSeq_getSize_r(handle, s, &size); 53 | for (int i = 0; i < size; ++i) { 54 | double x; 55 | GEOSCoordSeq_getX_r(handle, s, i, &x); 56 | if (x < *minX) { 57 | *minX = x; 58 | } 59 | if (x > *maxX) { 60 | *maxX = x; 61 | } 62 | double y; 63 | GEOSCoordSeq_getY_r(handle, s, i, &y); 64 | if (y < *minY) { 65 | *minY = y; 66 | } 67 | if (y > *maxY) { 68 | *maxY = y; 69 | } 70 | } 71 | } break; 72 | case GEOS_POLYGON: 73 | c_GEOSGeomBounds_r(handle, GEOSGetExteriorRing_r(handle, g), minX, minY, 74 | maxX, maxY); 75 | for (int i = 0, n = GEOSGetNumInteriorRings_r(handle, g); i < n; ++i) { 76 | c_GEOSGeomBounds_r(handle, GEOSGetInteriorRingN_r(handle, g, i), minX, 77 | minY, maxX, maxY); 78 | } 79 | break; 80 | case GEOS_MULTIPOINT: 81 | // fallthrough 82 | case GEOS_MULTILINESTRING: 83 | // fallthrough 84 | case GEOS_MULTIPOLYGON: 85 | // fallthrough 86 | case GEOS_GEOMETRYCOLLECTION: 87 | for (int i = 0, n = GEOSGetNumGeometries_r(handle, g); i < n; ++i) { 88 | c_GEOSGeomBounds_r(handle, GEOSGetGeometryN_r(handle, g, i), minX, minY, 89 | maxX, maxY); 90 | } 91 | break; 92 | } 93 | } 94 | 95 | // c_GEOSGeomGetInfo_r returns information about g. It returns 0 on any 96 | // exception, 1 otherwise. 97 | int c_GEOSGeomGetInfo_r(GEOSContextHandle_t handle, const GEOSGeometry *g, 98 | int *typeID, int *numGeometries, int *numPoints, 99 | int *numInteriorRings) { 100 | *typeID = GEOSGeomTypeId_r(handle, g); 101 | if (*typeID == -1) { 102 | return 0; 103 | } 104 | *numGeometries = GEOSGetNumGeometries_r(handle, g); 105 | if (*numGeometries == -1) { 106 | return 0; 107 | } 108 | switch (*typeID) { 109 | case GEOS_LINESTRING: 110 | // fallthrough 111 | case GEOS_LINEARRING: 112 | *numPoints = GEOSGeomGetNumPoints_r(handle, g); 113 | if (*numPoints == -1) { 114 | return 0; 115 | } 116 | break; 117 | case GEOS_POLYGON: 118 | *numInteriorRings = GEOSGetNumInteriorRings_r(handle, g); 119 | if (*numInteriorRings == -1) { 120 | return 0; 121 | } 122 | break; 123 | } 124 | return 1; 125 | } 126 | 127 | void c_errorMessageHandler(const char *message, void *userdata) { 128 | void go_errorMessageHandler(const char *, void *); 129 | go_errorMessageHandler(message, userdata); 130 | } 131 | 132 | // c_newGEOSGeomFromBounds_r returns a new GEOSGeom representing bounds. It 133 | // returns NULL on any exception. 134 | GEOSGeometry *c_newGEOSGeomFromBounds_r(GEOSContextHandle_t handle, int *typeID, 135 | double minX, double minY, double maxX, 136 | double maxY) { 137 | if (minX > maxX || minY > maxY) { 138 | *typeID = GEOS_POINT; 139 | return GEOSGeom_createEmptyPoint_r(handle); 140 | } 141 | if (minX == maxX && minY == maxY) { 142 | GEOSCoordSequence *s = GEOSCoordSeq_create_r(handle, 1, 2); 143 | if (s == NULL) { 144 | return NULL; 145 | } 146 | if (GEOSCoordSeq_setX_r(handle, s, 0, minX) == 0 || 147 | GEOSCoordSeq_setY_r(handle, s, 0, minY) == 0) { 148 | GEOSCoordSeq_destroy_r(handle, s); 149 | return NULL; 150 | } 151 | GEOSGeometry *g = GEOSGeom_createPoint_r(handle, s); 152 | if (g == NULL) { 153 | GEOSCoordSeq_destroy_r(handle, s); 154 | return NULL; 155 | } 156 | *typeID = GEOS_POINT; 157 | return g; 158 | } 159 | const double flatCoords[10] = {minX, minY, maxX, minY, maxX, 160 | maxY, minX, maxY, minX, minY}; 161 | GEOSCoordSequence *s = 162 | GEOSCoordSeq_copyFromBuffer_r(handle, flatCoords, 5, 0, 0); 163 | if (s == NULL) { 164 | return NULL; 165 | } 166 | GEOSGeometry *shell = GEOSGeom_createLinearRing_r(handle, s); 167 | if (shell == NULL) { 168 | GEOSCoordSeq_destroy_r(handle, s); 169 | return NULL; 170 | } 171 | GEOSGeometry *polygon = GEOSGeom_createPolygon_r(handle, shell, NULL, 0); 172 | if (polygon == NULL) { 173 | GEOSGeom_destroy_r(handle, shell); 174 | return NULL; 175 | } 176 | *typeID = GEOS_POLYGON; 177 | return polygon; 178 | } 179 | 180 | void c_GEOSSTRtree_query_callback(void *elem, void *userdata) { 181 | void go_GEOSSTRtree_query_callback(void *, void *); 182 | go_GEOSSTRtree_query_callback(elem, userdata); 183 | } 184 | 185 | int c_GEOSSTRtree_distance_callback(const void *item1, const void *item2, 186 | double *distance, void *userdata) { 187 | int go_GEOSSTRtree_distance_callback(const void *, const void *, double *, 188 | void *); 189 | return go_GEOSSTRtree_distance_callback(item1, item2, distance, userdata); 190 | } 191 | 192 | GEOSGeometry *c_GEOSMakeValidWithParams_r(GEOSContextHandle_t handle, 193 | const GEOSGeometry *g, 194 | enum GEOSMakeValidMethods method, 195 | int keepCollapsed) { 196 | GEOSGeometry *res; 197 | GEOSMakeValidParams *par; 198 | 199 | par = GEOSMakeValidParams_create_r(handle); 200 | GEOSMakeValidParams_setKeepCollapsed_r(handle, par, keepCollapsed); 201 | GEOSMakeValidParams_setMethod_r(handle, par, method); 202 | 203 | res = GEOSMakeValidWithParams_r(handle, g, par); 204 | 205 | GEOSMakeValidParams_destroy_r(handle, par); 206 | 207 | return res; 208 | } 209 | 210 | #if GEOS_VERSION_MAJOR < 3 || \ 211 | (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR < 11) 212 | 213 | GEOSGeometry *GEOSConcaveHull_r(GEOSContextHandle_t handle, 214 | const GEOSGeometry *g, double ratio, 215 | unsigned int allowHoles) { 216 | return NULL; 217 | } 218 | 219 | #endif 220 | 221 | #if GEOS_VERSION_MAJOR < 3 || \ 222 | (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR < 12) 223 | 224 | GEOSGeometry *GEOSConcaveHullByLength_r(GEOSContextHandle_t handle, 225 | const GEOSGeometry *g, double ratio, 226 | unsigned int allowHoles) { 227 | return NULL; 228 | } 229 | 230 | char GEOSPreparedContainsXY_r(GEOSContextHandle_t handle, 231 | const GEOSPreparedGeometry *pg1, double x, 232 | double y) { 233 | return 0; 234 | } 235 | 236 | char GEOSPreparedIntersectsXY_r(GEOSContextHandle_t handle, 237 | const GEOSPreparedGeometry *pg1, double x, 238 | double y) { 239 | return 0; 240 | } 241 | 242 | GEOSGeometry *GEOSDisjointSubsetUnion_r(GEOSContextHandle_t handle, 243 | const GEOSGeometry *g) { 244 | return NULL; 245 | } 246 | 247 | #endif 248 | -------------------------------------------------------------------------------- /go-geos.h: -------------------------------------------------------------------------------- 1 | #ifndef GEOS_H 2 | #define GEOS_H 3 | 4 | #include 5 | 6 | #define GEOS_USE_ONLY_R_API 7 | #include 8 | 9 | uintptr_t c_GEOSGeom_getUserData_r(GEOSContextHandle_t handle, 10 | const GEOSGeometry *g); 11 | void c_GEOSGeom_setUserData_r(GEOSContextHandle_t handle, GEOSGeometry *g, 12 | uintptr_t userdata); 13 | void c_GEOSGeomBounds_r(GEOSContextHandle_t handle, const GEOSGeometry *g, 14 | double *minX, double *minY, double *maxX, double *maxY); 15 | int c_GEOSGeomGetInfo_r(GEOSContextHandle_t handle, const GEOSGeometry *g, 16 | int *typeID, int *numGeometries, int *numPoints, 17 | int *numInteriorRings); 18 | void c_errorMessageHandler(const char *message, void *userdata); 19 | GEOSCoordSequence *c_newGEOSCoordSeqFromFlatCoords_r(GEOSContextHandle_t handle, 20 | unsigned int size, 21 | unsigned int dims, 22 | const double *flatCoords); 23 | GEOSGeometry *c_newGEOSGeomFromBounds_r(GEOSContextHandle_t handle, int *typeID, 24 | double minX, double minY, double maxX, 25 | double maxY); 26 | int c_GEOSSTRtree_distance_callback(const void *item1, const void *item2, 27 | double *distance, void *userdata); 28 | void c_GEOSSTRtree_query_callback(void *elem, void *userdata); 29 | GEOSGeometry *c_GEOSMakeValidWithParams_r(GEOSContextHandle_t handle, 30 | const GEOSGeometry *g, 31 | enum GEOSMakeValidMethods method, 32 | int keepCollapsed); 33 | 34 | #if GEOS_VERSION_MAJOR < 3 || \ 35 | (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR < 11) 36 | 37 | GEOSGeometry *GEOSConcaveHull_r(GEOSContextHandle_t handle, 38 | const GEOSGeometry *g, double ratio, 39 | unsigned int allowHoles); 40 | 41 | #endif 42 | 43 | #if GEOS_VERSION_MAJOR < 3 || \ 44 | (GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR < 12) 45 | 46 | GEOSGeometry *GEOSConcaveHullByLength_r(GEOSContextHandle_t handle, 47 | const GEOSGeometry *g, double ratio, 48 | unsigned int allowHoles); 49 | 50 | char GEOSPreparedContainsXY_r(GEOSContextHandle_t handle, 51 | const GEOSPreparedGeometry *pg1, double x, 52 | double y); 53 | 54 | char GEOSPreparedIntersectsXY_r(GEOSContextHandle_t handle, 55 | const GEOSPreparedGeometry *pg1, double x, 56 | double y); 57 | 58 | GEOSGeometry *GEOSDisjointSubsetUnion_r(GEOSContextHandle_t handle, 59 | const GEOSGeometry *g); 60 | 61 | #endif 62 | 63 | #endif 64 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/twpayne/go-geos 2 | 3 | go 1.24.0 4 | 5 | tool github.com/twpayne/go-geos/internal/cmds/execute-template 6 | 7 | require ( 8 | github.com/alecthomas/assert/v2 v2.11.0 9 | github.com/goccy/go-yaml v1.18.0 10 | ) 11 | 12 | require ( 13 | github.com/alecthomas/repr v0.4.0 // indirect 14 | github.com/hexops/gotextdiff v1.0.3 // indirect 15 | ) 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 2 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 3 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 4 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 5 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 6 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 7 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 8 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 9 | -------------------------------------------------------------------------------- /internal/cmds/execute-template/main.go: -------------------------------------------------------------------------------- 1 | // execute-template executes a Go template with data. 2 | package main 3 | 4 | import ( 5 | "bytes" 6 | "errors" 7 | "flag" 8 | "fmt" 9 | "go/format" 10 | "os" 11 | "path" 12 | "regexp" 13 | "slices" 14 | "text/template" 15 | "unicode" 16 | 17 | "github.com/goccy/go-yaml" 18 | ) 19 | 20 | var ( 21 | templateDataFilename = flag.String("data", "", "data filename") 22 | outputFilename = flag.String("output", "", "output filename") 23 | 24 | cTypes = map[string]string{ 25 | "BufCapStyle": "C.int", 26 | "BufJoinStyle": "C.int", 27 | "PrecisionRule": "C.int", 28 | "RelateBoundaryNodeRule": "C.int", 29 | "float64": "C.double", 30 | "int": "C.int", 31 | "uint": "C.unsigned", 32 | } 33 | ) 34 | 35 | func run() error { 36 | flag.Parse() 37 | 38 | var templateData []map[string]any 39 | if *templateDataFilename != "" { 40 | dataBytes, err := os.ReadFile(*templateDataFilename) 41 | if err != nil { 42 | return err 43 | } 44 | if err := yaml.Unmarshal(dataBytes, &templateData); err != nil { 45 | return err 46 | } 47 | } 48 | 49 | if !slices.IsSortedFunc(templateData, func(a, b map[string]any) int { 50 | switch aName, bName := a["name"].(string), b["name"].(string); { //nolint:forcetypeassert 51 | case aName < bName: 52 | return -1 53 | case aName == bName: 54 | return 0 55 | default: 56 | return 1 57 | } 58 | }) { 59 | return errors.New("template data not sorted by name") 60 | } 61 | 62 | if flag.NArg() == 0 { 63 | return errors.New("no arguments") 64 | } 65 | 66 | templateName := path.Base(flag.Arg(0)) 67 | buffer := &bytes.Buffer{} 68 | funcMap := template.FuncMap{ 69 | "cType": func(goType string) string { 70 | cType, ok := cTypes[goType] 71 | if !ok { 72 | panic(errors.New(goType + ": unknown C type for Go type")) 73 | } 74 | return cType 75 | }, 76 | "fatal": func(s string) string { 77 | panic(s) 78 | }, 79 | "firstRuneToLower": func(s string) string { 80 | runes := []rune(s) 81 | runes[0] = unicode.ToLower(runes[0]) 82 | return string(runes) 83 | }, 84 | "replaceAllRegexp": func(expr, repl, s string) string { 85 | return regexp.MustCompile(expr).ReplaceAllString(s, repl) 86 | }, 87 | } 88 | tmpl, err := template.New(templateName).Funcs(funcMap).ParseFiles(flag.Args()...) 89 | if err != nil { 90 | return err 91 | } 92 | if err := tmpl.Execute(buffer, templateData); err != nil { 93 | return err 94 | } 95 | 96 | output, err := format.Source(buffer.Bytes()) 97 | if err != nil { 98 | output = buffer.Bytes() 99 | } 100 | 101 | if *outputFilename == "" { 102 | if _, err := os.Stdout.Write(output); err != nil { 103 | return err 104 | } 105 | } else if data, err := os.ReadFile(*outputFilename); err != nil || !bytes.Equal(data, output) { 106 | //nolint:gosec 107 | if err := os.WriteFile(*outputFilename, output, 0o666); err != nil { 108 | return err 109 | } 110 | } 111 | 112 | return nil 113 | } 114 | 115 | func main() { 116 | if err := run(); err != nil { 117 | fmt.Println(err) 118 | os.Exit(1) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /internal/scripts/guess-missing-methods.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | comm -23 <(rg -INo '\bGEOS[A-Z_a-z]+_r\b' /usr/include/geos_c.h | sort | uniq) <(rg -INo '\bGEOS[A-Z_a-z]+_r\b' *.c *.go | sort | uniq) -------------------------------------------------------------------------------- /prepgeom.go: -------------------------------------------------------------------------------- 1 | package geos 2 | 3 | // #include "go-geos.h" 4 | import "C" 5 | 6 | import "runtime" 7 | 8 | // A PrepGeom is a prepared geometry. 9 | type PrepGeom struct { 10 | parent *Geom 11 | pgeom *C.struct_GEOSPrepGeom_t 12 | } 13 | 14 | // Prepare prepares g. 15 | func (g *Geom) Prepare() *PrepGeom { 16 | g.context.Lock() 17 | defer g.context.Unlock() 18 | pg := &PrepGeom{ 19 | parent: g, 20 | pgeom: C.GEOSPrepare_r(g.context.handle, g.geom), 21 | } 22 | runtime.SetFinalizer(pg, (*PrepGeom).Destroy) 23 | return pg 24 | } 25 | 26 | // Contains returns if pg contains g. 27 | func (pg *PrepGeom) Contains(g *Geom) bool { 28 | pg.parent.context.Lock() 29 | defer pg.parent.context.Unlock() 30 | if g.context != pg.parent.context { 31 | g.context.Lock() 32 | defer g.context.Unlock() 33 | } 34 | switch C.GEOSPreparedContains_r(pg.parent.context.handle, pg.pgeom, g.geom) { 35 | case 0: 36 | return false 37 | case 1: 38 | return true 39 | default: 40 | panic(pg.parent.context.err) 41 | } 42 | } 43 | 44 | // ContainsProperly returns if pg contains g properly. 45 | func (pg *PrepGeom) ContainsProperly(g *Geom) bool { 46 | pg.parent.context.Lock() 47 | defer pg.parent.context.Unlock() 48 | if g.context != pg.parent.context { 49 | g.context.Lock() 50 | defer g.context.Unlock() 51 | } 52 | switch C.GEOSPreparedContainsProperly_r(pg.parent.context.handle, pg.pgeom, g.geom) { 53 | case 0: 54 | return false 55 | case 1: 56 | return true 57 | default: 58 | panic(pg.parent.context.err) 59 | } 60 | } 61 | 62 | // ContainsXY returns if pg contains the point (x, y). 63 | func (pg *PrepGeom) ContainsXY(x, y float64) bool { 64 | pg.parent.context.Lock() 65 | defer pg.parent.context.Unlock() 66 | switch C.GEOSPreparedContainsXY_r(pg.parent.context.handle, pg.pgeom, C.double(x), C.double(y)) { 67 | case 0: 68 | return false 69 | case 1: 70 | return true 71 | default: 72 | panic(pg.parent.context.err) 73 | } 74 | } 75 | 76 | // CoveredBy returns if pg is covered by g. 77 | func (pg *PrepGeom) CoveredBy(g *Geom) bool { 78 | pg.parent.context.Lock() 79 | defer pg.parent.context.Unlock() 80 | if g.context != pg.parent.context { 81 | g.context.Lock() 82 | defer g.context.Unlock() 83 | } 84 | switch C.GEOSPreparedCoveredBy_r(pg.parent.context.handle, pg.pgeom, g.geom) { 85 | case 0: 86 | return false 87 | case 1: 88 | return true 89 | default: 90 | panic(pg.parent.context.err) 91 | } 92 | } 93 | 94 | // Covers returns if pg covers g. 95 | func (pg *PrepGeom) Covers(g *Geom) bool { 96 | pg.parent.context.Lock() 97 | defer pg.parent.context.Unlock() 98 | if g.context != pg.parent.context { 99 | g.context.Lock() 100 | defer g.context.Unlock() 101 | } 102 | switch C.GEOSPreparedCovers_r(pg.parent.context.handle, pg.pgeom, g.geom) { 103 | case 0: 104 | return false 105 | case 1: 106 | return true 107 | default: 108 | panic(pg.parent.context.err) 109 | } 110 | } 111 | 112 | // Crosses returns if pg crosses g. 113 | func (pg *PrepGeom) Crosses(g *Geom) bool { 114 | pg.parent.context.Lock() 115 | defer pg.parent.context.Unlock() 116 | if g.context != pg.parent.context { 117 | g.context.Lock() 118 | defer g.context.Unlock() 119 | } 120 | switch C.GEOSPreparedCrosses_r(pg.parent.context.handle, pg.pgeom, g.geom) { 121 | case 0: 122 | return false 123 | case 1: 124 | return true 125 | default: 126 | panic(pg.parent.context.err) 127 | } 128 | } 129 | 130 | // Destroy destroys pg and all resources associated with s. 131 | func (pg *PrepGeom) Destroy() { 132 | if pg == nil || pg.parent == nil || pg.parent.context == nil { 133 | return 134 | } 135 | pg.parent.context.Lock() 136 | defer pg.parent.context.Unlock() 137 | C.GEOSPreparedGeom_destroy_r(pg.parent.context.handle, pg.pgeom) 138 | *pg = PrepGeom{} // Clear all references. 139 | } 140 | 141 | // Disjoint returns if pg is disjoint from g. 142 | func (pg *PrepGeom) Disjoint(g *Geom) bool { 143 | pg.parent.context.Lock() 144 | defer pg.parent.context.Unlock() 145 | if g.context != pg.parent.context { 146 | g.context.Lock() 147 | defer g.context.Unlock() 148 | } 149 | switch C.GEOSPreparedDisjoint_r(pg.parent.context.handle, pg.pgeom, g.geom) { 150 | case 0: 151 | return false 152 | case 1: 153 | return true 154 | default: 155 | panic(pg.parent.context.err) 156 | } 157 | } 158 | 159 | // DistanceWithin returns if pg is within dist g. 160 | func (pg *PrepGeom) DistanceWithin(g *Geom, dist float64) bool { 161 | pg.parent.context.Lock() 162 | defer pg.parent.context.Unlock() 163 | if g.context != pg.parent.context { 164 | g.context.Lock() 165 | defer g.context.Unlock() 166 | } 167 | switch C.GEOSPreparedDistanceWithin_r(pg.parent.context.handle, pg.pgeom, g.geom, C.double(dist)) { 168 | case 0: 169 | return false 170 | case 1: 171 | return true 172 | default: 173 | panic(pg.parent.context.err) 174 | } 175 | } 176 | 177 | // Intersects returns if pg contains g. 178 | func (pg *PrepGeom) Intersects(g *Geom) bool { 179 | pg.parent.context.Lock() 180 | defer pg.parent.context.Unlock() 181 | if g.context != pg.parent.context { 182 | g.context.Lock() 183 | defer g.context.Unlock() 184 | } 185 | switch C.GEOSPreparedIntersects_r(pg.parent.context.handle, pg.pgeom, g.geom) { 186 | case 0: 187 | return false 188 | case 1: 189 | return true 190 | default: 191 | panic(pg.parent.context.err) 192 | } 193 | } 194 | 195 | // IntersectsXY returns if pg intersects the point at (x, y). 196 | func (pg *PrepGeom) IntersectsXY(x, y float64) bool { 197 | pg.parent.context.Lock() 198 | defer pg.parent.context.Unlock() 199 | switch C.GEOSPreparedIntersectsXY_r(pg.parent.context.handle, pg.pgeom, C.double(x), C.double(y)) { 200 | case 0: 201 | return false 202 | case 1: 203 | return true 204 | default: 205 | panic(pg.parent.context.err) 206 | } 207 | } 208 | 209 | // NearestPoints returns if pg overlaps g. 210 | func (pg *PrepGeom) NearestPoints(g *Geom) *CoordSeq { 211 | pg.parent.context.Lock() 212 | defer pg.parent.context.Unlock() 213 | if g.context != pg.parent.context { 214 | g.context.Lock() 215 | defer g.context.Unlock() 216 | } 217 | return pg.parent.context.newNonNilCoordSeq(C.GEOSPreparedNearestPoints_r(pg.parent.context.handle, pg.pgeom, g.geom)) 218 | } 219 | 220 | // Overlaps returns if pg overlaps g. 221 | func (pg *PrepGeom) Overlaps(g *Geom) bool { 222 | pg.parent.context.Lock() 223 | defer pg.parent.context.Unlock() 224 | if g.context != pg.parent.context { 225 | g.context.Lock() 226 | defer g.context.Unlock() 227 | } 228 | switch C.GEOSPreparedOverlaps_r(pg.parent.context.handle, pg.pgeom, g.geom) { 229 | case 0: 230 | return false 231 | case 1: 232 | return true 233 | default: 234 | panic(pg.parent.context.err) 235 | } 236 | } 237 | 238 | // Touches returns if pg contains g. 239 | func (pg *PrepGeom) Touches(g *Geom) bool { 240 | pg.parent.context.Lock() 241 | defer pg.parent.context.Unlock() 242 | if g.context != pg.parent.context { 243 | g.context.Lock() 244 | defer g.context.Unlock() 245 | } 246 | switch C.GEOSPreparedTouches_r(pg.parent.context.handle, pg.pgeom, g.geom) { 247 | case 0: 248 | return false 249 | case 1: 250 | return true 251 | default: 252 | panic(pg.parent.context.err) 253 | } 254 | } 255 | 256 | // Within returns if pg is within g. 257 | func (pg *PrepGeom) Within(g *Geom) bool { 258 | pg.parent.context.Lock() 259 | defer pg.parent.context.Unlock() 260 | if g.context != pg.parent.context { 261 | g.context.Lock() 262 | defer g.context.Unlock() 263 | } 264 | switch C.GEOSPreparedWithin_r(pg.parent.context.handle, pg.pgeom, g.geom) { 265 | case 0: 266 | return false 267 | case 1: 268 | return true 269 | default: 270 | panic(pg.parent.context.err) 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /prepgeom_test.go: -------------------------------------------------------------------------------- 1 | package geos_test 2 | 3 | import ( 4 | "runtime" 5 | "testing" 6 | 7 | "github.com/alecthomas/assert/v2" 8 | 9 | "github.com/twpayne/go-geos" 10 | ) 11 | 12 | func TestPrepGeom(t *testing.T) { 13 | defer runtime.GC() // Exercise finalizers. 14 | c := geos.NewContext() 15 | unitSquare := mustNewGeomFromWKT(t, c, "POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))").Prepare() 16 | middleSquare := mustNewGeomFromWKT(t, c, "POLYGON ((0.25 0.25, 0.25 0.75, 0.75 0.75, 0.75 0.25, 0.25 0.25))") 17 | assert.True(t, unitSquare.Contains(middleSquare)) 18 | assert.True(t, unitSquare.ContainsProperly(middleSquare)) 19 | if geos.VersionCompare(3, 12, 0) >= 0 { 20 | assert.True(t, unitSquare.ContainsXY(0.5, 0.5)) 21 | } 22 | assert.False(t, unitSquare.ContainsXY(2, 2)) 23 | assert.False(t, unitSquare.CoveredBy(middleSquare)) 24 | assert.True(t, unitSquare.Covers(middleSquare)) 25 | assert.False(t, unitSquare.Crosses(middleSquare)) 26 | assert.False(t, unitSquare.Disjoint(middleSquare)) 27 | assert.False(t, unitSquare.DistanceWithin(mustNewGeomFromWKT(t, c, "POINT (1.5 0.5)"), 0.1)) 28 | assert.True(t, unitSquare.Intersects(middleSquare)) 29 | if geos.VersionCompare(3, 12, 0) >= 0 { 30 | assert.True(t, unitSquare.IntersectsXY(0.5, 0.5)) 31 | assert.False(t, unitSquare.IntersectsXY(2, 2)) 32 | } 33 | assert.Equal(t, [][]float64{{1, 1}, {2, 2}}, unitSquare.NearestPoints(mustNewGeomFromWKT(t, c, "POINT (2 2)")).ToCoords()) 34 | assert.False(t, unitSquare.Overlaps(middleSquare)) 35 | assert.False(t, unitSquare.Touches(middleSquare)) 36 | assert.False(t, unitSquare.Within(middleSquare)) 37 | } 38 | -------------------------------------------------------------------------------- /strtree.go: -------------------------------------------------------------------------------- 1 | package geos 2 | 3 | // #include 4 | // #include "go-geos.h" 5 | import "C" 6 | 7 | import ( 8 | "runtime/cgo" 9 | "unsafe" 10 | ) 11 | 12 | // An STRtree is an R-tree spatial index structure for two dimensional data. 13 | type STRtree struct { 14 | context *Context 15 | strTree *C.struct_GEOSSTRtree_t 16 | itemToValue map[unsafe.Pointer]any 17 | valueToItem map[any]unsafe.Pointer 18 | } 19 | 20 | // Destroy frees all resources associated with t. 21 | func (t *STRtree) Destroy() { 22 | if t == nil || t.context == nil { 23 | return 24 | } 25 | t.context.Lock() 26 | defer t.context.Unlock() 27 | C.GEOSSTRtree_destroy_r(t.context.handle, t.strTree) 28 | for item := range t.itemToValue { 29 | C.free(item) 30 | } 31 | *t = STRtree{} // Clear all references. 32 | } 33 | 34 | // Insert inserts value with geometry g. 35 | func (t *STRtree) Insert(g *Geom, value any) error { 36 | if g.context != t.context { 37 | panic(errContextMismatch) 38 | } 39 | t.context.Lock() 40 | defer t.context.Unlock() 41 | if _, ok := t.valueToItem[value]; ok { 42 | return errDuplicateValue 43 | } 44 | item := C.calloc(1, C.size_t(unsafe.Sizeof(uintptr(0)))) 45 | t.itemToValue[item] = value 46 | t.valueToItem[value] = item 47 | C.GEOSSTRtree_insert_r(t.context.handle, t.strTree, g.geom, item) 48 | return nil 49 | } 50 | 51 | // Iterate calls f for every value in the t. 52 | func (t *STRtree) Iterate(callback func(any)) { 53 | handle := cgo.NewHandle(func(item unsafe.Pointer) { 54 | callback(t.itemToValue[item]) 55 | }) 56 | defer handle.Delete() 57 | t.context.Lock() 58 | defer t.context.Unlock() 59 | C.GEOSSTRtree_iterate_r( 60 | t.context.handle, 61 | t.strTree, 62 | (*[0]byte)(C.c_GEOSSTRtree_query_callback), // FIXME understand why the cast to *[0]byte is needed 63 | unsafe.Pointer(&handle), //nolint:gocritic 64 | ) 65 | } 66 | 67 | // Nearest returns the nearest item in t to value. 68 | func (t *STRtree) Nearest(value any, valueEnvelope *Geom, geomfn func(any) *Geom) any { 69 | if t.context != valueEnvelope.context { 70 | panic(errContextMismatch) 71 | } 72 | handle := cgo.NewHandle(func(item1, item2 unsafe.Pointer, distance *C.double) C.int { 73 | geom1 := geomfn(t.itemToValue[item1]) 74 | if geom1 == nil { 75 | return 0 76 | } 77 | geom2 := geomfn(t.itemToValue[item2]) 78 | if geom2 == nil { 79 | return 0 80 | } 81 | return C.GEOSDistance_r(t.context.handle, geom1.geom, geom2.geom, distance) 82 | }) 83 | defer handle.Delete() 84 | t.context.Lock() 85 | defer t.context.Unlock() 86 | nearestItem := C.GEOSSTRtree_nearest_generic_r( 87 | t.context.handle, 88 | t.strTree, 89 | t.valueToItem[value], 90 | valueEnvelope.geom, 91 | (*[0]byte)(C.c_GEOSSTRtree_distance_callback), // FIXME understand why the cast to *[0]byte is needed 92 | unsafe.Pointer(&handle), //nolint:gocritic 93 | ) 94 | return t.itemToValue[nearestItem] 95 | } 96 | 97 | // Query calls f with each value that intersects g. 98 | func (t *STRtree) Query(g *Geom, callback func(any)) { 99 | handle := cgo.NewHandle(func(elem unsafe.Pointer) { 100 | callback(t.itemToValue[elem]) 101 | }) 102 | defer handle.Delete() 103 | t.context.Lock() 104 | defer t.context.Unlock() 105 | C.GEOSSTRtree_query_r( 106 | t.context.handle, 107 | t.strTree, 108 | g.geom, 109 | (*[0]byte)(C.c_GEOSSTRtree_query_callback), // FIXME understand why the cast to *[0]byte is needed 110 | unsafe.Pointer(&handle), //nolint:gocritic 111 | ) 112 | } 113 | 114 | // Remove removes value with geometry g from t. 115 | func (t *STRtree) Remove(g *Geom, value any) bool { 116 | if g.context != t.context { 117 | panic(errContextMismatch) 118 | } 119 | item := t.valueToItem[value] 120 | t.context.Lock() 121 | defer t.context.Unlock() 122 | switch C.GEOSSTRtree_remove_r(t.context.handle, t.strTree, g.geom, item) { 123 | case 0: 124 | return false 125 | case 1: 126 | delete(t.valueToItem, value) 127 | delete(t.itemToValue, item) 128 | C.free(item) 129 | return true 130 | default: 131 | panic(t.context.err) 132 | } 133 | } 134 | 135 | func (t *STRtree) finalize() { 136 | if t.context == nil { 137 | return 138 | } 139 | if t.context.strTreeFinalizeFunc != nil { 140 | t.context.strTreeFinalizeFunc(t) 141 | } 142 | t.Destroy() 143 | } 144 | 145 | //export go_GEOSSTRtree_distance_callback 146 | func go_GEOSSTRtree_distance_callback(item1, item2 unsafe.Pointer, distance *C.double, userdata unsafe.Pointer) C.int { 147 | handle := *(*cgo.Handle)(userdata) 148 | f := handle.Value().(func(unsafe.Pointer, unsafe.Pointer, *C.double) C.int) //nolint:forcetypeassert,revive 149 | return f(item1, item2, distance) 150 | } 151 | 152 | //export go_GEOSSTRtree_query_callback 153 | func go_GEOSSTRtree_query_callback(elem, userdata unsafe.Pointer) { 154 | handle := *(*cgo.Handle)(userdata) 155 | handle.Value().(func(unsafe.Pointer))(elem) //nolint:forcetypeassert 156 | } 157 | -------------------------------------------------------------------------------- /strtree_test.go: -------------------------------------------------------------------------------- 1 | package geos_test 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/alecthomas/assert/v2" 9 | 10 | "github.com/twpayne/go-geos" 11 | ) 12 | 13 | func TestSTRtree(t *testing.T) { 14 | defer runtime.GC() // Exercise finalizers. 15 | c := geos.NewContext() 16 | 17 | tree := c.NewSTRtree(4) 18 | 19 | allItems := func() map[any]struct{} { 20 | result := make(map[any]struct{}) 21 | tree.Iterate(func(value any) { 22 | result[value] = struct{}{} 23 | }) 24 | return result 25 | } 26 | assert.Equal(t, map[any]struct{}{}, allItems()) 27 | 28 | g1 := mustNewGeomFromWKT(t, c, "POINT (0 0)") 29 | assert.NoError(t, tree.Insert(g1, 1)) 30 | assert.Equal(t, map[any]struct{}{ 31 | 1: {}, 32 | }, allItems()) 33 | 34 | g2 := mustNewGeomFromWKT(t, c, "POINT (0 2)") 35 | assert.NoError(t, tree.Insert(g2, 2)) 36 | assert.Equal(t, map[any]struct{}{ 37 | 1: {}, 38 | 2: {}, 39 | }, allItems()) 40 | 41 | items := make(map[any]struct{}) 42 | tree.Query(mustNewGeomFromWKT(t, c, "POLYGON ((-1 -1,1 -1,1 1,-1 1,-1 -1))"), func(value any) { 43 | items[value] = struct{}{} 44 | }) 45 | assert.Equal(t, map[any]struct{}{ 46 | 1: {}, 47 | }, items) 48 | 49 | assert.True(t, tree.Remove(g1, 1)) 50 | if false { 51 | // Items removed with GEOSSTRtree_remove_r are still returned by 52 | // STRtree.Iterate. See https://github.com/libgeos/geos/issues/833. 53 | assert.Equal(t, map[any]struct{}{ 54 | 2: {}, 55 | }, allItems()) 56 | } 57 | 58 | items2 := make(map[any]struct{}) 59 | tree.Query(mustNewGeomFromWKT(t, c, "POLYGON ((-1 -1,1 -1,1 1,-1 1,-1 -1))"), func(value any) { 60 | items2[value] = struct{}{} 61 | }) 62 | assert.Equal(t, map[any]struct{}{}, items2) 63 | } 64 | 65 | func TestSTRtreeNearest(t *testing.T) { 66 | defer runtime.GC() // Exercise finalizers. 67 | c := geos.NewContext() 68 | 69 | tree := c.NewSTRtree(8) 70 | g1 := mustNewGeomFromWKT(t, c, "POINT (0 1)") 71 | assert.NoError(t, tree.Insert(g1, g1)) 72 | g2 := mustNewGeomFromWKT(t, c, "POINT (0 2)") 73 | assert.NoError(t, tree.Insert(g2, g2)) 74 | g4 := mustNewGeomFromWKT(t, c, "POINT (0 4)") 75 | assert.NoError(t, tree.Insert(g4, g4)) 76 | 77 | assert.Equal(t, asAny(g2), tree.Nearest(g1, g1, asGeom)) 78 | assert.Equal(t, asAny(g1), tree.Nearest(g2, g2, asGeom)) 79 | assert.Equal(t, asAny(g2), tree.Nearest(g4, g4, asGeom)) 80 | } 81 | 82 | func TestSTRtreeLoad(t *testing.T) { 83 | defer runtime.GC() // Exercise finalizers. 84 | c := geos.NewContext() 85 | 86 | points := make(map[[2]int]*geos.Geom, 256*256) 87 | for x := range 256 { 88 | for y := range 256 { 89 | value := [2]int{x, y} 90 | points[value] = c.NewPoint([]float64{float64(x), float64(y)}) 91 | } 92 | } 93 | 94 | tree := c.NewSTRtree(8) 95 | for value, geom := range points { 96 | assert.NoError(t, tree.Insert(geom, value)) 97 | } 98 | 99 | items := make(map[[2]int]struct{}) 100 | tree.Query(mustNewGeomFromWKT(t, c, "POLYGON ((0 0,256 0,256 256,0 256,0 0))"), func(v any) { 101 | value, ok := v.([2]int) 102 | assert.True(t, ok) 103 | items[value] = struct{}{} 104 | }) 105 | assert.Equal(t, 256*256, len(items)) 106 | 107 | for x := range 256 { 108 | for y := range 256 { 109 | if (x+y)%2 == 0 { 110 | value := [2]int{x, y} 111 | assert.True(t, tree.Remove(points[value], value)) 112 | } 113 | } 114 | } 115 | 116 | runtime.GC() 117 | 118 | itemsAfterRemove := make(map[[2]int]struct{}) 119 | tree.Query(mustNewGeomFromWKT(t, c, "POLYGON ((0 0,256 0,256 256,0 256,0 0))"), func(value any) { 120 | array, ok := value.([2]int) 121 | assert.True(t, ok) 122 | itemsAfterRemove[array] = struct{}{} 123 | }) 124 | assert.Equal(t, 256*256/2, len(itemsAfterRemove)) 125 | } 126 | 127 | func asGeom(x any) *geos.Geom { 128 | g, ok := x.(*geos.Geom) 129 | if !ok { 130 | panic(fmt.Sprintf("%v is not a *geos.Geom", x)) 131 | } 132 | return g 133 | } 134 | 135 | func asAny(x any) any { 136 | return x 137 | } 138 | --------------------------------------------------------------------------------