├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── doc.go ├── go.mod ├── go.sum ├── node.go ├── node_test.go ├── rect.go ├── rect_test.go ├── tree.go └── tree_test.go /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # ci workflow 2 | name: ci 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | permissions: 13 | actions: read 14 | contents: read 15 | security-events: write 16 | 17 | jobs: 18 | lint: 19 | runs-on: ubuntu-latest 20 | environment: 21 | name: ci 22 | steps: 23 | - name: checkout 24 | uses: actions/checkout@v4 25 | - name: setup golang 26 | uses: actions/setup-go@v5 27 | with: 28 | go-version: ^1.24 29 | - name: golangci-lint 30 | uses: golangci/golangci-lint-action@v7 31 | test: 32 | runs-on: ubuntu-latest 33 | environment: 34 | name: ci 35 | steps: 36 | - name: checkout 37 | uses: actions/checkout@v4 38 | - name: setup golang 39 | uses: actions/setup-go@v5 40 | with: 41 | go-version: ^1.24 42 | check-latest: true 43 | - name: run-test 44 | run: make test 45 | - name: push-coverage 46 | if: ${{ github.event_name == 'push' }} 47 | uses: qltysh/qlty-action/coverage@v1 48 | with: 49 | token: ${{ secrets.QLTY_COVERAGE_TOKEN }} 50 | files: ${{ github.workspace }}/cover.out 51 | codeql: 52 | if: github.event_name == 'push' 53 | runs-on: ubuntu-latest 54 | environment: 55 | name: ci 56 | steps: 57 | - name: checkout 58 | uses: actions/checkout@v4 59 | - name: setup golang 60 | uses: actions/setup-go@v5 61 | with: 62 | go-version: ^1.24 63 | check-latest: true 64 | cache: true 65 | - name: init codeql 66 | uses: github/codeql-action/init@v3 67 | with: 68 | languages: 'go' 69 | - name: run analysis 70 | uses: github/codeql-action/analyze@v3 71 | - name: update goreportcard 72 | uses: creekorful/goreportcard-action@v1.0 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.out 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | allow-parallel-runners: true 4 | linters: 5 | default: all 6 | disable: 7 | - ireturn 8 | - intrange 9 | - recvcheck 10 | - exhaustruct 11 | - nonamedreturns 12 | - testpackage 13 | - varnamelen 14 | settings: 15 | errcheck: 16 | check-type-assertions: true 17 | gocritic: 18 | enabled-tags: 19 | - performance 20 | - opinionated 21 | - diagnostic 22 | - style 23 | exclusions: 24 | generated: lax 25 | presets: 26 | - comments 27 | - common-false-positives 28 | - legacy 29 | - std-error-handling 30 | rules: 31 | - linters: 32 | - gochecknoglobals 33 | - gosec 34 | - unparam 35 | path: ._test\.go 36 | paths: 37 | - third_party$ 38 | - builtin$ 39 | - examples$ 40 | formatters: 41 | enable: 42 | - gci 43 | - gofmt 44 | - goimports 45 | exclusions: 46 | generated: lax 47 | paths: 48 | - third_party$ 49 | - builtin$ 50 | - examples$ 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Alexei Shevchenko 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | COP=cover.out 2 | 3 | .PHONY: vet lint test test-cover clean 4 | 5 | vet: 6 | @- go vet ./... 7 | 8 | lint: vet 9 | @- golangci-lint run 10 | 11 | test: vet 12 | @- go test -race -count 1 -v -coverprofile="$(COP)" ./... 13 | 14 | test-cover: test 15 | @- go tool cover -func="$(COP)" 16 | 17 | bench: 18 | @- go test -count 1 -bench=. -benchmem -timeout 15m 19 | 20 | bench-profile: 21 | @- go test -count 1 -bench=. -benchmem -timeout 15m -cpuprofile=cpu.out -memprofile=mem.out 22 | 23 | clean: 24 | @- rm -f "$(COP)" 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/s0rg/quadtree)](https://pkg.go.dev/github.com/s0rg/quadtree) 2 | [![License](https://img.shields.io/badge/license-MIT%20License-blue.svg)](https://github.com/s0rg/quadtree/blob/master/LICENSE) 3 | [![Go Version](https://img.shields.io/github/go-mod/go-version/s0rg/quadtree)](go.mod) 4 | [![Tag](https://img.shields.io/github/v/tag/s0rg/quadtree?sort=semver)](https://github.com/s0rg/quadtree/tags) 5 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go) 6 | 7 | [![CI](https://github.com/s0rg/quadtree/workflows/ci/badge.svg)](https://github.com/s0rg/quadtree/actions?query=workflow%3Aci) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/s0rg/quadtree)](https://goreportcard.com/report/github.com/s0rg/quadtree) 9 | [![Maintainability](https://qlty.sh/badges/ae1e81be-3dc3-4cf6-ae43-cd4b1344e765/maintainability.svg)](https://qlty.sh/gh/s0rg/projects/quadtree) 10 | [![Code Coverage](https://qlty.sh/badges/ae1e81be-3dc3-4cf6-ae43-cd4b1344e765/test_coverage.svg)](https://qlty.sh/gh/s0rg/projects/quadtree) 11 | ![Issues](https://img.shields.io/github/issues/s0rg/quadtree) 12 | 13 | # quadtree 14 | 15 | [Quadtree](https://en.wikipedia.org/wiki/Quadtree) for golang. 16 | 17 | # features 18 | 19 | - generic 20 | - heavy optimized 21 | - zero-alloc 22 | - 100% test coverage 23 | 24 | # example 25 | ```go 26 | import ( 27 | "log" 28 | 29 | "github.com/s0rg/quadtree" 30 | ) 31 | 32 | func main() { 33 | // width, height and max depth for new tree 34 | tree := quadtree.New[int](100.0, 100.0, 4) 35 | 36 | // add some points 37 | tree.Add(10.0, 10.0, 5.0, 5.0, 1) 38 | tree.Add(15.0, 20.0, 10.0, 10.0, 2) 39 | tree.Add(40.0, 10.0, 4.0, 4.0, 3) 40 | tree.Add(90.0, 90.0, 5.0, 8.0, 4) 41 | 42 | val, ok := tree.Get(9.0, 9.0, 11.0, 11.0) 43 | if !ok { 44 | log.Fatal("not found") 45 | } 46 | 47 | log.Println(val) // should print 1 48 | 49 | const ( 50 | distance = 20.0 51 | count = 2 52 | ) 53 | 54 | tree.KNearest(80.0, 80.0, distance, count, func(x, y, w, h float64, val int) { 55 | log.Printf("(%f, %f, %f, %f) = %d", x, y, w, h, val) 56 | }) 57 | 58 | // output: (90.000000, 90.000000, 5.000000, 8.000000) = 4 59 | } 60 | ``` 61 | 62 | # benchmark 63 | 64 | ``` 65 | goos: linux 66 | goarch: amd64 67 | pkg: github.com/s0rg/quadtree 68 | cpu: AMD Ryzen 5 5500U with Radeon Graphics 69 | BenchmarkNode/Insert-12 14974236 71.07 ns/op 249 B/op 0 allocs/op 70 | BenchmarkNode/Del-12 6415672 188.3 ns/op 0 B/op 0 allocs/op 71 | BenchmarkNode/Search-12 21702474 51.83 ns/op 0 B/op 0 allocs/op 72 | BenchmarkTree/Add-12 18840514 67.83 ns/op 241 B/op 0 allocs/op 73 | BenchmarkTree/Get-12 21204722 55.46 ns/op 0 B/op 0 allocs/op 74 | BenchmarkTree/Move-12 8061322 147.5 ns/op 0 B/op 0 allocs/op 75 | BenchmarkTree/ForEach-12 18723290 58.60 ns/op 0 B/op 0 allocs/op 76 | BenchmarkTree/KNearest-12 3595956 324.7 ns/op 0 B/op 0 allocs/op 77 | BenchmarkTree/Del-12 6234123 193.1 ns/op 0 B/op 0 allocs/op 78 | PASS 79 | ok github.com/s0rg/quadtree 12.666s 80 | ``` 81 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package quadtree is a generic, zero-alloc Quadtree. 3 | */ 4 | package quadtree 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/s0rg/quadtree 2 | 3 | go 1.24 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s0rg/quadtree/97508e2359c33e17ef6834219d1eebcddefbbe5a/go.sum -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package quadtree 2 | 3 | const childCount = 4 4 | 5 | type item[T any] struct { 6 | Value T 7 | Rect rect 8 | } 9 | 10 | type node[T any] struct { 11 | Childs []*node[T] 12 | Items []item[T] 13 | Rect rect 14 | } 15 | 16 | func makeNode[T any](r rect) *node[T] { 17 | return &node[T]{ 18 | Rect: r, 19 | Childs: []*node[T]{}, 20 | } 21 | } 22 | 23 | func (n *node[T]) Grow(want, cur int) { 24 | if cur > want { 25 | return 26 | } 27 | 28 | n.Childs = make([]*node[T], childCount) 29 | 30 | for i, r := range n.Rect.Split() { 31 | n.Childs[i] = makeNode[T](r) 32 | n.Childs[i].Grow(want, cur+1) 33 | } 34 | } 35 | 36 | func (n *node[T]) Insert(r rect, value T) (ok bool) { 37 | if !n.Rect.ContainsRect(r) { 38 | return 39 | } 40 | 41 | for i := 0; i < len(n.Childs); i++ { 42 | if c := n.Childs[i]; c.Rect.ContainsRect(r) { 43 | return c.Insert(r, value) 44 | } 45 | } 46 | 47 | n.Items = append(n.Items, item[T]{Rect: r, Value: value}) 48 | 49 | return true 50 | } 51 | 52 | func (n *node[T]) Search(r rect, iter func(*item[T]) bool) { 53 | if !n.Rect.Overlaps(r) { 54 | return 55 | } 56 | 57 | var it *item[T] 58 | 59 | for i := 0; i < len(n.Items); i++ { 60 | if it = &n.Items[i]; !it.Rect.Overlaps(r) { 61 | continue 62 | } 63 | 64 | if !iter(it) { 65 | return 66 | } 67 | } 68 | 69 | for i := 0; i < len(n.Childs); i++ { 70 | switch { 71 | case r.ContainsRect(n.Childs[i].Rect): 72 | if !n.Childs[i].ForEach(iter) { 73 | return 74 | } 75 | case n.Childs[i].Rect.Overlaps(r): 76 | n.Childs[i].Search(r, iter) 77 | } 78 | } 79 | } 80 | 81 | func (n *node[T]) ForEach(iter func(*item[T]) bool) (next bool) { 82 | for i := 0; i < len(n.Items); i++ { 83 | if !iter(&n.Items[i]) { 84 | return false 85 | } 86 | } 87 | 88 | for i := 0; i < len(n.Childs); i++ { 89 | if !n.Childs[i].ForEach(iter) { 90 | return false 91 | } 92 | } 93 | 94 | return true 95 | } 96 | 97 | func (n *node[T]) Size() (total int) { 98 | total = len(n.Items) 99 | 100 | for i := 0; i < len(n.Childs); i++ { 101 | total += n.Childs[i].Size() 102 | } 103 | 104 | return total 105 | } 106 | 107 | func (n *node[T]) Del(x, y float64) (it item[T], ok bool) { 108 | if !n.Rect.ContainsPoint(x, y) { 109 | return 110 | } 111 | 112 | for i := 0; i < len(n.Items); i++ { 113 | if it = n.Items[i]; it.Rect.ContainsPoint(x, y) { 114 | last := len(n.Items) - 1 115 | 116 | if i != last { 117 | n.Items[i] = n.Items[last] 118 | } 119 | 120 | n.Items = n.Items[:last] 121 | 122 | return it, true 123 | } 124 | } 125 | 126 | for i := 0; i < len(n.Childs); i++ { 127 | if it, ok = n.Childs[i].Del(x, y); ok { 128 | return it, ok 129 | } 130 | } 131 | 132 | return 133 | } 134 | -------------------------------------------------------------------------------- /node_test.go: -------------------------------------------------------------------------------- 1 | package quadtree 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | const nodeTestDepth = 4 10 | 11 | func TestNodeInsertSearchOK(t *testing.T) { 12 | t.Parallel() 13 | 14 | n := makeNode[int](rect{0, 0, 80, 80}) 15 | n.Grow(nodeTestDepth, 0) 16 | 17 | if s := n.Size(); s != 0 { 18 | t.Fatalf("empty but size is: %d", s) 19 | } 20 | 21 | n.Insert(rect{1, 1, 5, 5}, 1) 22 | n.Insert(rect{20, 23, 25, 28}, 2) 23 | n.Insert(rect{30, 31, 35, 55}, 3) 24 | n.Insert(rect{36, 32, 39, 58}, 4) 25 | 26 | if s := n.Size(); s != 4 { 27 | t.Fatalf("expected size 3 but size is: %d", s) 28 | } 29 | 30 | var found int 31 | 32 | n.Search(rect{29, 30, 40, 60}, func(it *item[int]) bool { 33 | switch it.Value { 34 | case 3, 4: 35 | found++ 36 | default: 37 | t.Fatalf("wrong value found: %d at (%f, %f)", it.Value, it.Rect.X0, it.Rect.Y0) 38 | } 39 | 40 | return (found < 2) 41 | }) 42 | 43 | if found != 2 { 44 | t.Fatalf("wrong found count: %d", found) 45 | } 46 | 47 | n.Search(rect{18, 18, 30, 30}, func(it *item[int]) bool { 48 | if it.Value != 2 { 49 | t.Fatalf("wrong value found: %d at (%f, %f)", it.Value, it.Rect.X0, it.Rect.Y0) 50 | } 51 | 52 | return false 53 | }) 54 | } 55 | 56 | func TestNodeInsertSearchOOB(t *testing.T) { 57 | t.Parallel() 58 | 59 | n := makeNode[int](rect{10, 10, 80, 80}) 60 | n.Grow(nodeTestDepth, 0) 61 | 62 | if n.Insert(rect{1, 1, 5, 5}, 1) { 63 | t.Fatal("out-of-bound insert 1") 64 | } 65 | 66 | if n.Insert(rect{120, 23, 125, 28}, 2) { 67 | t.Fatal("out-of-bound insert 2") 68 | } 69 | 70 | if n.Size() != 0 { 71 | t.Fatal("out-of-bound insert size") 72 | } 73 | 74 | n.Search(rect{90, 10, 100, 5}, func(it *item[int]) bool { 75 | t.Fatalf("found out-of-bound: %d at (%f, %f)", it.Value, it.Rect.X0, it.Rect.Y0) 76 | 77 | return false 78 | }) 79 | } 80 | 81 | func TestNodeDel(t *testing.T) { 82 | t.Parallel() 83 | 84 | n := makeNode[int](rect{0, 0, 80, 80}) 85 | n.Grow(nodeTestDepth/2, 0) 86 | 87 | n.Insert(rect{1, 1, 5, 5}, 1) 88 | n.Insert(rect{20, 23, 25, 28}, 2) 89 | n.Insert(rect{30, 31, 35, 55}, 3) 90 | n.Insert(rect{26, 26, 28, 28}, 4) 91 | 92 | n.Del(100, 100) 93 | 94 | if n.Size() != 4 { 95 | t.Fatal("del remove out-of-bound") 96 | } 97 | 98 | n.Del(70, 70) 99 | 100 | if n.Size() != 4 { 101 | t.Fatal("del remove unexisted") 102 | } 103 | 104 | n.Del(21, 24) 105 | 106 | if n.Size() != 3 { 107 | t.Fatal("del doesnt remove 2") 108 | } 109 | 110 | n.Search(rect{18, 18, 30, 30}, func(it *item[int]) bool { 111 | if it.Value == 2 { 112 | t.Fatalf("2 still exists at (%f, %f)", it.Rect.X0, it.Rect.Y0) 113 | } 114 | 115 | return false 116 | }) 117 | 118 | n.Del(32, 42) 119 | 120 | if n.Size() != 2 { 121 | t.Fatal("del doesnt remove 3") 122 | } 123 | 124 | n.Search(rect{31, 32, 31, 32}, func(it *item[int]) bool { 125 | if it.Value == 3 { 126 | t.Fatalf("3 still exists %d at (%f, %f)", it.Value, it.Rect.X0, it.Rect.Y0) 127 | } 128 | 129 | return false 130 | }) 131 | } 132 | 133 | func BenchmarkNode(b *testing.B) { 134 | const ( 135 | benchmarkSide = 1000.0 136 | benchmarkSize = 100 137 | maxSize = 10.0 138 | ) 139 | 140 | node := makeNode[int](rc(0, 0, benchmarkSide, benchmarkSide)) 141 | node.Grow(nodeTestDepth, 0) 142 | 143 | rc := randRect(benchmarkSide-maxSize, maxSize) 144 | x, y := randPoint(benchmarkSide - maxSize) 145 | 146 | b.ResetTimer() 147 | 148 | b.Run("Insert", func(b *testing.B) { 149 | for n := 0; n < b.N; n++ { 150 | node.Insert(rc, n) 151 | } 152 | }) 153 | 154 | b.Run("Del", func(b *testing.B) { 155 | for n := 0; n < b.N; n++ { 156 | node.Del(x, y) 157 | } 158 | }) 159 | 160 | b.Run("Search", func(b *testing.B) { 161 | for n := 0; n < b.N; n++ { 162 | node.Search(rc, func(_ *item[int]) bool { return false }) 163 | } 164 | }) 165 | } 166 | 167 | var rng = rand.New(rand.NewSource(time.Now().UnixNano())) 168 | 169 | func randFloat(_min, _max float64) (rv float64) { 170 | return _min + (rng.Float64() * (_max - _min)) 171 | } 172 | 173 | func randRect(maxPos, maxSide float64) (r rect) { 174 | x, y := randFloat(1, maxPos-1), randFloat(1, maxPos-1) 175 | w, h := randFloat(1, maxSide), randFloat(1, maxSide) 176 | 177 | return rc(x, y, w, h) 178 | } 179 | 180 | func randPoint(maxPos float64) (x, y float64) { 181 | x, y = randFloat(1, maxPos-1), randFloat(1, maxPos-1) 182 | 183 | return 184 | } 185 | -------------------------------------------------------------------------------- /rect.go: -------------------------------------------------------------------------------- 1 | package quadtree 2 | 3 | const half = 2.0 4 | 5 | type rect struct { 6 | X0, Y0, X1, Y1 float64 7 | } 8 | 9 | func rc(x, y, w, h float64) (r rect) { 10 | return rect{ 11 | X0: x, 12 | Y0: y, 13 | X1: x + w, 14 | Y1: y + h, 15 | } 16 | } 17 | 18 | func (r rect) Pad(d float64) (rv rect) { 19 | return rect{ 20 | X0: r.X0 - d, 21 | Y0: r.Y0 - d, 22 | X1: r.X1 + d, 23 | Y1: r.Y1 + d, 24 | } 25 | } 26 | 27 | func (r rect) Clip(b rect) (rv rect) { 28 | if r.X0 < b.X0 { 29 | r.X0 = b.X0 30 | } 31 | 32 | if r.Y0 < b.Y0 { 33 | r.Y0 = b.Y0 34 | } 35 | 36 | if r.X1 > b.X1 { 37 | r.X1 = b.X1 38 | } 39 | 40 | if r.Y1 > b.Y1 { 41 | r.Y1 = b.Y1 42 | } 43 | 44 | return r 45 | } 46 | 47 | func (r *rect) Center() (x, y float64) { 48 | dx, dy := r.Width()/half, r.Heigth()/half 49 | 50 | return r.X0 + dx, r.Y0 + dy 51 | } 52 | 53 | func (r *rect) Width() float64 { 54 | return r.X1 - r.X0 55 | } 56 | 57 | func (r *rect) Heigth() float64 { 58 | return r.Y1 - r.Y0 59 | } 60 | 61 | func (r *rect) ContainsPoint(x, y float64) (yes bool) { 62 | if x < r.X0 || y < r.Y0 { 63 | return 64 | } 65 | 66 | if x > r.X1 || y > r.Y1 { 67 | return 68 | } 69 | 70 | return true 71 | } 72 | 73 | func (r *rect) ContainsRect(v rect) (yes bool) { 74 | if !r.ContainsPoint(v.X0, v.Y0) { 75 | return 76 | } 77 | 78 | if !r.ContainsPoint(v.X1, v.Y1) { 79 | return 80 | } 81 | 82 | return true 83 | } 84 | 85 | func (r *rect) Overlaps(v rect) (yes bool) { 86 | return r.X0 < v.X1 && r.X1 >= v.X0 && r.Y0 < v.Y1 && r.Y1 >= v.Y0 87 | } 88 | 89 | func (r *rect) Split() (rv [4]rect) { 90 | dx, dy := r.Width()/half, r.Heigth()/half 91 | 92 | return [4]rect{ 93 | {r.X0, r.Y0, r.X0 + dx, r.Y0 + dy}, // upper left 94 | {r.X0 + dx, r.Y0, r.X1, r.Y1 - dy}, // upper right 95 | {r.X0, r.Y0 + dy, r.X0 + dx, r.Y1}, // lower left 96 | {r.X0 + dx, r.Y0 + dy, r.X1, r.Y1}, // lower right 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /rect_test.go: -------------------------------------------------------------------------------- 1 | package quadtree 2 | 3 | import ( 4 | "math" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestContainsPoint(t *testing.T) { 10 | t.Parallel() 11 | 12 | type testCase struct { 13 | Rect rect 14 | X, Y float64 15 | Want bool 16 | } 17 | 18 | cases := []testCase{ 19 | {rect{0, 0, 10, 10}, 2, 2, true}, 20 | {rect{5, 5, 10, 10}, 1, 1, false}, 21 | {rect{5, 5, 10, 10}, 15, 15, false}, 22 | {rect{5, 5, 10, 10}, 6, 15, false}, 23 | {rect{5, 5, 10, 10}, 15, 6, false}, 24 | } 25 | 26 | for i, tc := range cases { 27 | if res := tc.Rect.ContainsPoint(tc.X, tc.Y); res != tc.Want { 28 | t.Fatalf("case[%d] failed, want %t got: %t", i, tc.Want, res) 29 | } 30 | } 31 | } 32 | 33 | func TestContainsRect(t *testing.T) { 34 | t.Parallel() 35 | 36 | type testCase struct { 37 | A, B rect 38 | Want bool 39 | } 40 | 41 | cases := []testCase{ 42 | {rect{0, 0, 10, 10}, rect{2, 2, 8, 8}, true}, 43 | {rect{5, 5, 10, 10}, rect{1, 1, 6, 6}, false}, 44 | {rect{5, 5, 10, 10}, rect{6, 6, 12, 12}, false}, 45 | } 46 | 47 | for i, tc := range cases { 48 | if res := tc.A.ContainsRect(tc.B); res != tc.Want { 49 | t.Fatalf("case[%d] failed, want %t got: %t", i, tc.Want, res) 50 | } 51 | } 52 | } 53 | 54 | func TestOverlaps(t *testing.T) { 55 | t.Parallel() 56 | 57 | type testCase struct { 58 | A, B rect 59 | Want bool 60 | } 61 | 62 | cases := []testCase{ 63 | {rect{0, 0, 10, 10}, rect{2, 2, 8, 8}, true}, 64 | {rect{5, 5, 10, 10}, rect{1, 1, 6, 6}, true}, 65 | {rect{5, 5, 10, 10}, rect{6, 6, 12, 12}, true}, 66 | {rect{5, 5, 10, 10}, rect{15, 0, 4, 4}, false}, 67 | } 68 | 69 | for i, tc := range cases { 70 | if res := tc.A.Overlaps(tc.B); res != tc.Want { 71 | t.Fatalf("case[%d] failed, want %t got: %t", i, tc.Want, res) 72 | } 73 | } 74 | } 75 | 76 | func TestSplit(t *testing.T) { 77 | t.Parallel() 78 | 79 | type testCase struct { 80 | Rect rect 81 | Want [4]rect 82 | } 83 | 84 | cases := []testCase{ 85 | { 86 | rect{0, 0, 8, 8}, 87 | [4]rect{ 88 | {0, 0, 4, 4}, 89 | {4, 0, 8, 4}, 90 | {0, 4, 4, 8}, 91 | {4, 4, 8, 8}, 92 | }, 93 | }, 94 | { 95 | rect{0, 0, 20, 20}, 96 | [4]rect{ 97 | {0, 0, 10, 10}, 98 | {10, 0, 20, 10}, 99 | {0, 10, 10, 20}, 100 | {10, 10, 20, 20}, 101 | }, 102 | }, 103 | } 104 | 105 | for i, tc := range cases { 106 | if res := tc.Rect.Split(); !reflect.DeepEqual(res, tc.Want) { 107 | t.Fatalf("case[%d] failed, want %v got: %v", i, tc.Want, res) 108 | } 109 | } 110 | } 111 | 112 | func TestDim(t *testing.T) { 113 | t.Parallel() 114 | 115 | const ( 116 | e = 0.0000001 117 | w = 8.0 118 | h = 9.0 119 | ) 120 | 121 | rc := rc(0, 0, w, h) 122 | 123 | if math.Abs(rc.Width()-w) > e { 124 | t.Fatal("width") 125 | } 126 | 127 | if math.Abs(rc.Heigth()-h) > e { 128 | t.Fatal("heigth") 129 | } 130 | } 131 | 132 | func TestPad(t *testing.T) { 133 | t.Parallel() 134 | 135 | const ( 136 | e = 20.0000001 137 | w = 8.0 138 | h = 9.0 139 | d = 10.0 140 | ) 141 | 142 | rc := rc(0, 0, w, h).Pad(d) 143 | 144 | if math.Abs(rc.Width()-w) > e { 145 | t.Fatal("width") 146 | } 147 | 148 | if math.Abs(rc.Heigth()-h) > e { 149 | t.Fatal("heigth") 150 | } 151 | } 152 | 153 | func TestClip(t *testing.T) { 154 | t.Parallel() 155 | 156 | const ( 157 | w = 20.0 158 | h = 15.0 159 | ) 160 | 161 | var area = rc(0, 0, w, h) 162 | 163 | var cases = []rect{ 164 | rc(-1, -1, 2, 4), 165 | rc(-1, 4, 4, 5), 166 | rc(4, -2, 10, 10), 167 | rc(19, 5, 6, 6), 168 | rc(6, 14, 5, 5), 169 | rc(25, 16, 5, 5), 170 | } 171 | 172 | for i, c := range cases { 173 | tc := c.Clip(area) 174 | 175 | if tc.X0 < area.X0 { 176 | t.Fatalf("case[%d] fail fot X0: %f", i, tc.X0) 177 | } 178 | 179 | if tc.X1 > area.X1 { 180 | t.Fatalf("case[%d] fail fot X1: %f", i, tc.X1) 181 | } 182 | 183 | if tc.Y0 < area.Y0 { 184 | t.Fatalf("case[%d] fail fot Y0", i) 185 | } 186 | 187 | if tc.Y1 > area.Y1 { 188 | t.Fatalf("case[%d] fail fot Y1", i) 189 | } 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package quadtree 2 | 3 | import "math" 4 | 5 | // Tree is a quad-tree container. 6 | type Tree[T any] struct { 7 | root *node[T] 8 | } 9 | 10 | // Iter is a shorthand for iterator callback type. 11 | type Iter[T any] func(x, y, w, h float64, value T) 12 | 13 | // New creates new, empty quad tree instance with given area width and height and maximum depth value. 14 | func New[T any](w, h float64, depth int) (rv *Tree[T]) { 15 | root := makeNode[T](rc(0, 0, w, h)) 16 | root.Grow(depth, 0) 17 | 18 | return &Tree[T]{ 19 | root: root, 20 | } 21 | } 22 | 23 | // Size returns current elements count. 24 | func (t *Tree[T]) Size() int { 25 | return t.root.Size() 26 | } 27 | 28 | // Add adds rectangle, with top-left corner at (x, y) and given width and height. 29 | func (t *Tree[T]) Add(x, y, w, h float64, value T) (ok bool) { 30 | return t.root.Insert(rc(x, y, w, h), value) 31 | } 32 | 33 | // Get returns item located at given coordinates, if any. 34 | func (t *Tree[T]) Get(x, y, w, h float64) (value T, ok bool) { 35 | area := rc(x, y, w, h).Clip(t.root.Rect) 36 | 37 | t.root.Search(area, func(it *item[T]) (next bool) { 38 | value, ok = it.Value, true 39 | 40 | return false 41 | }) 42 | 43 | return value, ok 44 | } 45 | 46 | // Del removes any items located at given coordinates, returns true if succeed. 47 | func (t *Tree[T]) Del(x, y float64) (ok bool) { 48 | _, ok = t.root.Del(x, y) 49 | 50 | return 51 | } 52 | 53 | // Move moves item located at (x, y) to (newX, newY), returns true if succeed. 54 | func (t *Tree[T]) Move(x, y, newX, newY float64) (ok bool) { 55 | var it item[T] 56 | 57 | if it, ok = t.root.Del(x, y); !ok { 58 | return 59 | } 60 | 61 | return t.Add(newX, newY, it.Rect.Width(), it.Rect.Heigth(), it.Value) 62 | } 63 | 64 | // ForEach iterates over items in given region. 65 | func (t *Tree[T]) ForEach(x, y, w, h float64, iter Iter[T]) { 66 | area := rc(x, y, w, h).Clip(t.root.Rect) 67 | 68 | t.root.Search(area, func(it *item[T]) (next bool) { 69 | iter(it.Rect.X0, it.Rect.Y0, it.Rect.Width(), it.Rect.Heigth(), it.Value) 70 | 71 | return true 72 | }) 73 | } 74 | 75 | // KNearest iterates over k nearest for given coordinates within given distance. 76 | func (t *Tree[T]) KNearest(x, y, distance float64, k int, iter Iter[T]) { 77 | var ( 78 | found int 79 | area = rc(x, y, x, y).Pad(distance).Clip(t.root.Rect) 80 | ) 81 | 82 | t.root.Search(area, func(it *item[T]) (next bool) { 83 | cx, cy := it.Rect.Center() 84 | 85 | if dist2d(x, y, cx, cy) <= distance { 86 | iter(it.Rect.X0, it.Rect.Y0, it.Rect.Width(), it.Rect.Heigth(), it.Value) 87 | 88 | found++ 89 | } 90 | 91 | return found < k 92 | }) 93 | } 94 | 95 | func dist2d(x1, y1, x2, y2 float64) (rv float64) { 96 | dx, dy := x2-x1, y2-y1 97 | 98 | return math.Sqrt(dx*dx + dy*dy) 99 | } 100 | -------------------------------------------------------------------------------- /tree_test.go: -------------------------------------------------------------------------------- 1 | package quadtree 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestAdd(t *testing.T) { 8 | t.Parallel() 9 | 10 | q := New[int](100, 100, 4) 11 | 12 | if q.Size() != 0 { 13 | t.Fatal("non-empty") 14 | } 15 | 16 | q.Add(10, 10, 5, 5, 1) 17 | q.Add(15, 20, 10, 10, 2) 18 | q.Add(40, 10, 4, 4, 3) 19 | q.Add(90, 90, 5, 8, 4) 20 | 21 | if q.Size() != 4 { 22 | t.Fatal("empty") 23 | } 24 | 25 | var ( 26 | val int 27 | ok bool 28 | ) 29 | 30 | if val, ok = q.Get(9, 9, 11, 11); !ok { 31 | t.Fatal("got nothing at (9, 9, 11, 11)") 32 | } 33 | 34 | if val != 1 { 35 | t.Fatalf("unexpected value: %d", val) 36 | } 37 | 38 | if _, ok = q.Get(101, 101, 111, 111); ok { 39 | t.Fatal("got something at (101, 101, 111, 111)") 40 | } 41 | 42 | if val, ok = q.Get(39, 9, 42, 12); !ok { 43 | t.Fatal("got nothing at (39, 9, 42, 12)") 44 | } 45 | 46 | if val != 3 { 47 | t.Fatalf("invalid value: %d", val) 48 | } 49 | } 50 | 51 | func TestDel(t *testing.T) { 52 | t.Parallel() 53 | 54 | q := New[int](100, 100, 4) 55 | 56 | q.Add(5, 5, 2, 2, 1) 57 | q.Add(10, 10, 3, 3, 2) 58 | q.Add(11, 11, 4, 4, 3) 59 | 60 | q.Del(12, 12) 61 | 62 | if s := q.Size(); s != 2 { 63 | t.Fatalf("unexpected len = %d", s) 64 | } 65 | } 66 | 67 | func TestForEach(t *testing.T) { 68 | t.Parallel() 69 | 70 | q := New[int](100, 100, 4) 71 | 72 | q.Add(5, 5, 2, 3, 1) 73 | q.Add(10, 10, 5, 5, 2) 74 | q.Add(11, 11, 3, 3, 3) 75 | q.Add(25, 25, 10, 10, 100) 76 | 77 | var sum int 78 | 79 | q.ForEach(1, 1, 20, 20, func(_, _, _, _ float64, val int) { 80 | sum += val 81 | }) 82 | 83 | const sumExpect = 6 84 | 85 | if sum != sumExpect { 86 | t.Fatalf("unexpected sum = %d", sum) 87 | } 88 | } 89 | 90 | func TestMove(t *testing.T) { 91 | t.Parallel() 92 | 93 | q := New[int](100, 100, 4) 94 | 95 | q.Add(5, 5, 10, 10, 1) 96 | 97 | var ( 98 | val int 99 | ok bool 100 | ) 101 | 102 | if val, ok = q.Get(4, 4, 6, 6); !ok || val != 1 { 103 | t.Fatal("step 1 unexpected") 104 | } 105 | 106 | if q.Move(55, 55, 0, 0) { 107 | t.Fatal("unexpected move success") 108 | } 109 | 110 | if !q.Move(5, 5, 50, 50) { 111 | t.Fatal("cannot move") 112 | } 113 | 114 | if val, ok = q.Get(45, 45, 55, 55); !ok || val != 1 { 115 | t.Fatal("step 2 unexpected") 116 | } 117 | } 118 | 119 | func TestKNearest(t *testing.T) { 120 | t.Parallel() 121 | 122 | q := New[int](50, 50, 4) 123 | 124 | q.Add(1, 1, 2, 2, 1) 125 | q.Add(5, 1, 2, 2, 2) 126 | q.Add(1, 5, 2, 2, 3) 127 | q.Add(10, 10, 2, 2, 10) 128 | q.Add(1, 10, 2, 2, 10) 129 | 130 | var sum int 131 | 132 | q.KNearest(3, 3, 5, 3, func(_, _, _, _ float64, val int) { 133 | sum += val 134 | }) 135 | 136 | const sumExpect = 6 137 | 138 | if sum != sumExpect { 139 | t.Fatalf("unexpected sum = %d", sum) 140 | } 141 | } 142 | 143 | func BenchmarkTree(b *testing.B) { 144 | const ( 145 | benchmarkSide = 1000.0 146 | maxSize = 10.0 147 | ) 148 | 149 | q := New[int](benchmarkSide, benchmarkSide, nodeTestDepth) 150 | 151 | x, y := 1.0, 1.0 152 | x2, y2 := 800.0, 800.0 153 | 154 | b.ResetTimer() 155 | 156 | b.Run("Add", func(b *testing.B) { 157 | for n := 0; n < b.N; n++ { 158 | q.Add(x, y, maxSize, maxSize, n) 159 | } 160 | }) 161 | 162 | b.Run("Get", func(b *testing.B) { 163 | for n := 0; n < b.N; n++ { 164 | _, _ = q.Get(x, y, maxSize, maxSize) 165 | } 166 | }) 167 | 168 | b.Run("Move", func(b *testing.B) { 169 | for n := 0; n < b.N; n++ { 170 | _ = q.Move(x, y, x2, y2) 171 | x, y, x2, y2 = x2, y2, x, y 172 | } 173 | }) 174 | 175 | b.Run("ForEach", func(b *testing.B) { 176 | for n := 0; n < b.N; n++ { 177 | q.ForEach(x, y, maxSize, maxSize, func(_, _, _, _ float64, _ int) {}) 178 | } 179 | }) 180 | 181 | b.Run("KNearest", func(b *testing.B) { 182 | for n := 0; n < b.N; n++ { 183 | q.KNearest(x, y, maxSize, int(maxSize), func(_, _, _, _ float64, _ int) {}) 184 | } 185 | }) 186 | 187 | b.Run("Del", func(b *testing.B) { 188 | for n := 0; n < b.N; n++ { 189 | _ = q.Del(x, y) 190 | } 191 | }) 192 | } 193 | --------------------------------------------------------------------------------