mirror of https://github.com/tidwall/tile38.git
Updated dependencies
This commit is contained in:
parent
ac45824c7d
commit
5ae1a76450
|
@ -223,12 +223,12 @@
|
|||
revision = "9876f1454cf0993a53d74c27196993e345f50dd1"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:2fef6390e8d9118debd4937a699afad9e1629f2d5d3c965a58fc33afe04f9b46"
|
||||
digest = "1:4d2ec831fbaaf74fd75d2d9fe107e605c92489ec6cef6d36e1f23b678e9f2bd4"
|
||||
name = "github.com/tidwall/buntdb"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "b67b1b8c1658cb01502801c14e33c61e6c4cbb95"
|
||||
revision = "6249481c29c2cd96f53b691b74ac1893f72774c2"
|
||||
version = "v1.1.0"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:91acf4d86b348c1f1832336836035373b047ffcb16a0fde066bd531bbe3452b2"
|
||||
|
@ -319,11 +319,14 @@
|
|||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9384bff0cadcd196d5981923a738898c36d4ce049781152b9a9816791c8221b4"
|
||||
digest = "1:5d9d865e55b95f001e52a7f5d1f812e8a80f0f05d5b04ede006f24206ebba33c"
|
||||
name = "github.com/tidwall/rtree"
|
||||
packages = ["."]
|
||||
packages = [
|
||||
".",
|
||||
"base",
|
||||
]
|
||||
pruneopts = ""
|
||||
revision = "d4a8a3d30d5729f85edfba1745241f3a621d0359"
|
||||
revision = "6cd427091e0e662cb4f8e2c9eb1a41e1c46ff0d3"
|
||||
|
||||
[[projects]]
|
||||
digest = "1:ca969d3e75ed5b3003f4f5864bb5c13d99471ef57f9049bf78562d7ee1ac019c"
|
||||
|
@ -341,6 +344,14 @@
|
|||
pruneopts = ""
|
||||
revision = "de5932d649b50053050d43056146b960f3d90ca5"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9d6562efe571b54b2ec08ed598e4ba08d77b966dc2103a4300ae0cd0286dd6c3"
|
||||
name = "github.com/tidwall/tinyqueue"
|
||||
packages = ["."]
|
||||
pruneopts = ""
|
||||
revision = "1e39f55115634cad2c504631c8bfcc292f2c9c55"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
digest = "1:9d71091ff8756d88318a4334be685d311b10e1a01c0290ce743187b3bfb1b3f6"
|
||||
|
|
|
@ -67,8 +67,8 @@ required = [
|
|||
name = "github.com/tidwall/btree"
|
||||
|
||||
[[constraint]]
|
||||
branch = "master"
|
||||
name = "github.com/tidwall/buntdb"
|
||||
version = "1.1.0"
|
||||
|
||||
[[constraint]]
|
||||
name = "github.com/tidwall/gjson"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<p align="center">
|
||||
<img
|
||||
src="logo.png"
|
||||
<img
|
||||
src="logo.png"
|
||||
width="307" height="150" border="0" alt="BuntDB">
|
||||
<br>
|
||||
<a href="https://travis-ci.org/tidwall/buntdb"><img src="https://img.shields.io/travis/tidwall/buntdb.svg?style=flat-square" alt="Build Status"></a>
|
||||
|
@ -9,15 +9,12 @@
|
|||
<a href="https://godoc.org/github.com/tidwall/buntdb"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
|
||||
</p>
|
||||
|
||||
BuntDB is a low-level, in-memory, key/value store in pure Go.
|
||||
BuntDB is a low-level, in-memory, key/value store in pure Go.
|
||||
It persists to disk, is ACID compliant, and uses locking for multiple
|
||||
readers and a single writer. It supports custom indexes and geospatial
|
||||
data. It's ideal for projects that need a dependable database and favor
|
||||
readers and a single writer. It supports custom indexes and geospatial
|
||||
data. It's ideal for projects that need a dependable database and favor
|
||||
speed over data size.
|
||||
|
||||
The desire to create BuntDB stems from the need for a new embeddable
|
||||
database for [Tile38](https://github.com/tidwall/tile38) and [SummitDB](https://github.com/tidwall/summitdb).
|
||||
|
||||
Features
|
||||
========
|
||||
|
||||
|
@ -52,7 +49,7 @@ This will retrieve the library.
|
|||
|
||||
## Opening a database
|
||||
|
||||
The primary object in BuntDB is a `DB`. To open or create your
|
||||
The primary object in BuntDB is a `DB`. To open or create your
|
||||
database, use the `buntdb.Open()` function:
|
||||
|
||||
```go
|
||||
|
@ -71,8 +68,8 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
...
|
||||
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -94,18 +91,18 @@ A read-only transaction should be used when you don't need to make changes to th
|
|||
|
||||
```go
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
...
|
||||
return nil
|
||||
...
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
### Read/write Transactions
|
||||
A read/write transaction is used when you need to make changes to your data. There can only be one read/write transaction running at a time. So make sure you close it as soon as you are done with it.
|
||||
A read/write transaction is used when you need to make changes to your data. There can only be one read/write transaction running at a time. So make sure you close it as soon as you are done with it.
|
||||
|
||||
```go
|
||||
err := db.Update(func(tx *buntdb.Tx) error {
|
||||
...
|
||||
return nil
|
||||
...
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -115,8 +112,8 @@ To set a value you must open a read/write transaction:
|
|||
|
||||
```go
|
||||
err := db.Update(func(tx *buntdb.Tx) error {
|
||||
_, _, err := tx.Set("mykey", "myvalue", nil)
|
||||
return err
|
||||
_, _, err := tx.Set("mykey", "myvalue", nil)
|
||||
return err
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -125,34 +122,32 @@ To get the value:
|
|||
|
||||
```go
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
val, err := tx.Get("mykey")
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
fmt.Printf("value is %s\n", val)
|
||||
return nil
|
||||
val, err := tx.Get("mykey")
|
||||
if err != nil{
|
||||
return err
|
||||
}
|
||||
fmt.Printf("value is %s\n", val)
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
Getting non-existent values will case an `ErrNotFound` error.
|
||||
Getting non-existent values will cause an `ErrNotFound` error.
|
||||
|
||||
### Iterating
|
||||
All keys/value pairs are ordered in the database by the key. To iterate over the keys:
|
||||
|
||||
```go
|
||||
err := db.View(func(tx *buntdb.Tx) error {
|
||||
err := tx.Ascend("", func(key, value string) bool{
|
||||
fmt.Printf("key: %s, value: %s\n", key, value)
|
||||
})
|
||||
return err
|
||||
err := tx.Ascend("", func(key, value string) bool {
|
||||
fmt.Printf("key: %s, value: %s\n", key, value)
|
||||
})
|
||||
return err
|
||||
})
|
||||
```
|
||||
|
||||
There is also `AscendGreaterOrEqual`, `AscendLessThan`, `AscendRange`, `AscendEqual`, `Descend`, `DescendLessOrEqual`, `DescendGreaterThan`, `DescendRange`, and `DescendEqual`. Please see the [documentation](https://godoc.org/github.com/tidwall/buntdb) for more information on these functions.
|
||||
|
||||
|
||||
|
||||
|
||||
## Custom Indexes
|
||||
Initially all data is stored in a single [B-tree](https://en.wikipedia.org/wiki/B-tree) with each item having one key and one value. All of these items are ordered by the key. This is great for quickly getting a value from a key or [iterating](#iterating) over the keys. Feel free to peruse the [B-tree implementation](https://github.com/tidwall/btree).
|
||||
|
||||
|
@ -170,7 +165,7 @@ Now you can add various names:
|
|||
|
||||
```go
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set("user:0:name", "tom", nil)
|
||||
tx.Set("user:0:name", "tom", nil)
|
||||
tx.Set("user:1:name", "Randi", nil)
|
||||
tx.Set("user:2:name", "jane", nil)
|
||||
tx.Set("user:4:name", "Janet", nil)
|
||||
|
@ -186,10 +181,10 @@ Finally you can iterate over the index:
|
|||
```go
|
||||
db.View(func(tx *buntdb.Tx) error {
|
||||
tx.Ascend("names", func(key, val string) bool {
|
||||
fmt.Printf(buf, "%s %s\n", key, val)
|
||||
fmt.Printf(buf, "%s %s\n", key, val)
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
```
|
||||
The output should be:
|
||||
|
@ -213,7 +208,7 @@ Now only items with keys that have the prefix `user:` will be added to the `name
|
|||
|
||||
|
||||
### Built-in types
|
||||
Along with `IndexString`, there is also `IndexInt`, `IndexUint`, and `IndexFloat`.
|
||||
Along with `IndexString`, there is also `IndexInt`, `IndexUint`, and `IndexFloat`.
|
||||
These are built-in types for indexing. You can choose to use these or create your own.
|
||||
|
||||
So to create an index that is numerically ordered on an age key, we could use:
|
||||
|
@ -226,7 +221,7 @@ And then add values:
|
|||
|
||||
```go
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set("user:0:age", "35", nil)
|
||||
tx.Set("user:0:age", "35", nil)
|
||||
tx.Set("user:1:age", "49", nil)
|
||||
tx.Set("user:2:age", "13", nil)
|
||||
tx.Set("user:4:age", "63", nil)
|
||||
|
@ -240,22 +235,22 @@ db.Update(func(tx *buntdb.Tx) error {
|
|||
```go
|
||||
db.View(func(tx *buntdb.Tx) error {
|
||||
tx.Ascend("ages", func(key, val string) bool {
|
||||
fmt.Printf(buf, "%s %s\n", key, val)
|
||||
fmt.Printf(buf, "%s %s\n", key, val)
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
The output should be:
|
||||
```
|
||||
user:6:name 3
|
||||
user:5:name 8
|
||||
user:2:name 13
|
||||
user:7:name 16
|
||||
user:0:name 35
|
||||
user:1:name 49
|
||||
user:4:name 63
|
||||
user:6:age 3
|
||||
user:5:age 8
|
||||
user:2:age 13
|
||||
user:7:age 16
|
||||
user:0:age 35
|
||||
user:1:age 49
|
||||
user:4:age 63
|
||||
```
|
||||
|
||||
## Spatial Indexes
|
||||
|
@ -273,7 +268,7 @@ To add some lon,lat points to the `fleet` index:
|
|||
|
||||
```go
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set("fleet:0:pos", "[-115.567 33.532]", nil)
|
||||
tx.Set("fleet:0:pos", "[-115.567 33.532]", nil)
|
||||
tx.Set("fleet:1:pos", "[-116.671 35.735]", nil)
|
||||
tx.Set("fleet:2:pos", "[-113.902 31.234]", nil)
|
||||
return nil
|
||||
|
@ -284,38 +279,52 @@ And then you can run the `Intersects` function on the index:
|
|||
|
||||
```go
|
||||
db.View(func(tx *buntdb.Tx) error {
|
||||
tx.Intersects("fleet", "[-117 30],[-112 36]", func(key, val string) bool {
|
||||
...
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
tx.Intersects("fleet", "[-117 30],[-112 36]", func(key, val string) bool {
|
||||
...
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
This will get all three positions.
|
||||
|
||||
### k-Nearest Neighbors
|
||||
|
||||
Use the `Nearby` function to get all the positions in order of nearest to farthest :
|
||||
|
||||
```go
|
||||
db.View(func(tx *buntdb.Tx) error {
|
||||
tx.Nearby("fleet", "[-113 33]", func(key, val string, dist float64) bool {
|
||||
...
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
### Spatial bracket syntax
|
||||
|
||||
The bracket syntax `[-117 30],[-112 36]` is unique to BuntDB, and it's how the built-in rectangles are processed. But, you are not limited to this syntax. Whatever Rect function you choose to use during `CreateSpatialIndex` will be used to process the parameter, in this case it's `IndexRect`.
|
||||
|
||||
- **2D rectangle:** `[10 15],[20 25]`
|
||||
- **2D rectangle:** `[10 15],[20 25]`
|
||||
*Min XY: "10x15", Max XY: "20x25"*
|
||||
|
||||
- **3D rectangle:** `[10 15 12],[20 25 18]`
|
||||
- **3D rectangle:** `[10 15 12],[20 25 18]`
|
||||
*Min XYZ: "10x15x12", Max XYZ: "20x25x18"*
|
||||
|
||||
- **2D point:** `[10 15]`
|
||||
- **2D point:** `[10 15]`
|
||||
*XY: "10x15"*
|
||||
|
||||
- **LatLon point:** `[-112.2693 33.5123]`
|
||||
- **LonLat point:** `[-112.2693 33.5123]`
|
||||
*LatLon: "33.5123 -112.2693"*
|
||||
|
||||
- **LatLon bounding box:** `[-112.26 33.51],[-112.18 33.67]`
|
||||
- **LonLat bounding box:** `[-112.26 33.51],[-112.18 33.67]`
|
||||
*Min LatLon: "33.51 -112.26", Max LatLon: "33.67 -112.18"*
|
||||
|
||||
**Notice:** The longitude is the Y axis and is on the left, and latitude is the X axis and is on the right.
|
||||
|
||||
You can also represent `Infinity` by using `-inf` and `+inf`.
|
||||
You can also represent `Infinity` by using `-inf` and `+inf`.
|
||||
For example, you might have the following points (`[X Y M]` where XY is a point and M is a timestamp):
|
||||
```
|
||||
[3 9 1]
|
||||
|
@ -330,8 +339,8 @@ You can then do a search for all points with `M` between 2-4 by calling `Interse
|
|||
|
||||
```go
|
||||
tx.Intersects("points", "[-inf -inf 2],[+inf +inf 4]", func(key, val string) bool {
|
||||
println(val)
|
||||
return true
|
||||
println(val)
|
||||
return true
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -410,7 +419,7 @@ Order by age range 30-50
|
|||
```
|
||||
|
||||
## Multi Value Index
|
||||
With BuntDB it's possible to join multiple values on a single index.
|
||||
With BuntDB it's possible to join multiple values on a single index.
|
||||
This is similar to a [multi column index](http://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html) in a traditional SQL database.
|
||||
|
||||
In this example we are creating a multi value index on "name.last" and "age":
|
||||
|
@ -448,9 +457,9 @@ db.View(func(tx *buntdb.Tx) error {
|
|||
Any index can be put in descending order by wrapping it's less function with `buntdb.Desc`.
|
||||
|
||||
```go
|
||||
db.CreateIndex("last_name_age", "*",
|
||||
buntdb.IndexJSON("name.last"),
|
||||
buntdb.Desc(buntdb.IndexJSON("age")))
|
||||
db.CreateIndex("last_name_age", "*",
|
||||
buntdb.IndexJSON("name.last"),
|
||||
buntdb.Desc(buntdb.IndexJSON("age")))
|
||||
```
|
||||
|
||||
This will create a multi value index where the last name is ascending and the age is descending.
|
||||
|
@ -474,9 +483,9 @@ import "github.com/tidwall/collate"
|
|||
// To sort case-insensitive in French.
|
||||
db.CreateIndex("name", "*", collate.IndexString("FRENCH_CI"))
|
||||
|
||||
// To specify that numbers should sort numerically ("2" < "12")
|
||||
// To specify that numbers should sort numerically ("2" < "12")
|
||||
// and use a comma to represent a decimal point.
|
||||
db.CreateIndex("amount", "*", collate.IndexString("FRENCH_NUM"))
|
||||
db.CreateIndex("amount", "*", collate.IndexString("FRENCH_NUM"))
|
||||
```
|
||||
|
||||
There's also support for Collation on JSON indexes:
|
||||
|
@ -492,16 +501,35 @@ Items can be automatically evicted by using the `SetOptions` object in the `Set`
|
|||
|
||||
```go
|
||||
db.Update(func(tx *buntdb.Tx) error {
|
||||
tx.Set("mykey", "myval", &buntdb.SetOptions{Expires:true, TTL:time.Second})
|
||||
tx.Set("mykey", "myval", &buntdb.SetOptions{Expires:true, TTL:time.Second})
|
||||
return nil
|
||||
})
|
||||
```
|
||||
|
||||
Now `mykey` will automatically be deleted after one second. You can remove the TTL by setting the value again with the same key/value, but with the options parameter set to nil.
|
||||
|
||||
## Delete while iterating
|
||||
BuntDB does not currently support deleting a key while in the process of iterating.
|
||||
As a workaround you'll need to delete keys following the completion of the iterator.
|
||||
|
||||
```go
|
||||
var delkeys []string
|
||||
tx.AscendKeys("object:*", func(k, v string) bool {
|
||||
if someCondition(k) == true {
|
||||
delkeys = append(delkeys, k)
|
||||
}
|
||||
return true // continue
|
||||
})
|
||||
for _, k := range delkeys {
|
||||
if _, err = tx.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Append-only File
|
||||
|
||||
BuntDB uses an AOF (append-only file) which is a log of all database changes that occur from operations like `Set()` and `Delete()`.
|
||||
BuntDB uses an AOF (append-only file) which is a log of all database changes that occur from operations like `Set()` and `Delete()`.
|
||||
|
||||
The format of this file looks like:
|
||||
```
|
||||
|
@ -531,7 +559,7 @@ The `Config.SyncPolicy` has the following options:
|
|||
- `EverySecond` - fsync every second, fast and safer, this is the default
|
||||
- `Always` - fsync after every write, very durable, slower
|
||||
|
||||
## Config
|
||||
## Config
|
||||
|
||||
Here are some configuration options that can be use to change various behaviors of the database.
|
||||
|
||||
|
@ -546,10 +574,10 @@ To update the configuration you should call `ReadConfig` followed by `SetConfig`
|
|||
|
||||
var config buntdb.Config
|
||||
if err := db.ReadConfig(&config); err != nil{
|
||||
log.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := db.WriteConfig(config); err != nil{
|
||||
log.Fatal(err)
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -557,7 +585,7 @@ if err := db.WriteConfig(config); err != nil{
|
|||
|
||||
How fast is BuntDB?
|
||||
|
||||
Here are some example [benchmarks](https://github.com/tidwall/raft-buntdb#raftstore-performance-comparison) when using BuntDB in a Raft Store implementation.
|
||||
Here are some example [benchmarks](https://github.com/tidwall/raft-buntdb#raftstore-performance-comparison) when using BuntDB in a Raft Store implementation.
|
||||
|
||||
You can also run the standard Go benchmark tool from the project root directory:
|
||||
|
||||
|
|
|
@ -121,6 +121,12 @@ type Config struct {
|
|||
// OnExpired is used to custom handle the deletion option when a key
|
||||
// has been expired.
|
||||
OnExpired func(keys []string)
|
||||
|
||||
// OnExpiredSync will be called inside the same transaction that is performing
|
||||
// the deletion of expired items. If OnExpired is present then this callback
|
||||
// will not be called. If this callback is present, then the deletion of the
|
||||
// timeed-out item is the explicit responsibility of this callback.
|
||||
OnExpiredSync func(key, value string, tx *Tx) error
|
||||
}
|
||||
|
||||
// exctx is a simple b-tree context for ordering by expiration.
|
||||
|
@ -544,9 +550,13 @@ func (db *DB) backgroundManager() {
|
|||
// Open a standard view. This will take a full lock of the
|
||||
// database thus allowing for access to anything we need.
|
||||
var onExpired func([]string)
|
||||
var expired []string
|
||||
var expired []*dbItem
|
||||
var onExpiredSync func(key, value string, tx *Tx) error
|
||||
err := db.Update(func(tx *Tx) error {
|
||||
onExpired = db.config.OnExpired
|
||||
if onExpired == nil {
|
||||
onExpiredSync = db.config.OnExpiredSync
|
||||
}
|
||||
if db.persist && !db.config.AutoShrinkDisabled {
|
||||
pos, err := db.file.Seek(0, 1)
|
||||
if err != nil {
|
||||
|
@ -562,12 +572,12 @@ func (db *DB) backgroundManager() {
|
|||
db.exps.AscendLessThan(&dbItem{
|
||||
opts: &dbItemOpts{ex: true, exat: time.Now()},
|
||||
}, func(item btree.Item) bool {
|
||||
expired = append(expired, item.(*dbItem).key)
|
||||
expired = append(expired, item.(*dbItem))
|
||||
return true
|
||||
})
|
||||
if onExpired == nil {
|
||||
for _, key := range expired {
|
||||
if _, err := tx.Delete(key); err != nil {
|
||||
if onExpired == nil && onExpiredSync == nil {
|
||||
for _, itm := range expired {
|
||||
if _, err := tx.Delete(itm.key); err != nil {
|
||||
// it's ok to get a "not found" because the
|
||||
// 'Delete' method reports "not found" for
|
||||
// expired items.
|
||||
|
@ -576,6 +586,12 @@ func (db *DB) backgroundManager() {
|
|||
}
|
||||
}
|
||||
}
|
||||
} else if onExpiredSync != nil {
|
||||
for _, itm := range expired {
|
||||
if err := onExpiredSync(itm.key, itm.val, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
@ -585,7 +601,11 @@ func (db *DB) backgroundManager() {
|
|||
|
||||
// send expired event, if needed
|
||||
if onExpired != nil && len(expired) > 0 {
|
||||
onExpired(expired)
|
||||
keys := make([]string, 0, 32)
|
||||
for _, itm := range expired {
|
||||
keys = append(keys, itm.key)
|
||||
}
|
||||
onExpired(keys)
|
||||
}
|
||||
|
||||
// execute a disk sync, if needed
|
||||
|
@ -1399,13 +1419,18 @@ func (tx *Tx) Set(key, value string, opts *SetOptions) (previousValue string,
|
|||
}
|
||||
|
||||
// Get returns a value for a key. If the item does not exist or if the item
|
||||
// has expired then ErrNotFound is returned.
|
||||
func (tx *Tx) Get(key string) (val string, err error) {
|
||||
// has expired then ErrNotFound is returned. If ignoreExpired is true, then
|
||||
// the found value will be returned even if it is expired.
|
||||
func (tx *Tx) Get(key string, ignoreExpired ...bool) (val string, err error) {
|
||||
if tx.db == nil {
|
||||
return "", ErrTxClosed
|
||||
}
|
||||
var ignore bool
|
||||
if len(ignoreExpired) != 0 {
|
||||
ignore = ignoreExpired[0]
|
||||
}
|
||||
item := tx.db.get(key)
|
||||
if item == nil || item.expired() {
|
||||
if item == nil || (item.expired() && !ignore) {
|
||||
// The item does not exists or has expired. Let's assume that
|
||||
// the caller is only interested in items that have not expired.
|
||||
return "", ErrNotFound
|
||||
|
@ -1775,7 +1800,7 @@ func (tx *Tx) DescendEqual(index, pivot string,
|
|||
})
|
||||
}
|
||||
|
||||
// rect is used by Intersects
|
||||
// rect is used by Intersects and Nearby
|
||||
type rect struct {
|
||||
min, max []float64
|
||||
}
|
||||
|
@ -1784,6 +1809,48 @@ func (r *rect) Rect(ctx interface{}) (min, max []float64) {
|
|||
return r.min, r.max
|
||||
}
|
||||
|
||||
// Nearby searches for rectangle items that are nearby a target rect.
|
||||
// All items belonging to the specified index will be returned in order of
|
||||
// nearest to farthest.
|
||||
// The specified index must have been created by AddIndex() and the target
|
||||
// is represented by the rect string. This string will be processed by the
|
||||
// same bounds function that was passed to the CreateSpatialIndex() function.
|
||||
// An invalid index will return an error.
|
||||
// The dist param is the distance of the bounding boxes. In the case of
|
||||
// simple 2D points, it's the distance of the two 2D points squared.
|
||||
func (tx *Tx) Nearby(index, bounds string,
|
||||
iterator func(key, value string, dist float64) bool) error {
|
||||
if tx.db == nil {
|
||||
return ErrTxClosed
|
||||
}
|
||||
if index == "" {
|
||||
// cannot search on keys tree. just return nil.
|
||||
return nil
|
||||
}
|
||||
// // wrap a rtree specific iterator around the user-defined iterator.
|
||||
iter := func(item rtree.Item, dist float64) bool {
|
||||
dbi := item.(*dbItem)
|
||||
return iterator(dbi.key, dbi.val, dist)
|
||||
}
|
||||
idx := tx.db.idxs[index]
|
||||
if idx == nil {
|
||||
// index was not found. return error
|
||||
return ErrNotFound
|
||||
}
|
||||
if idx.rtr == nil {
|
||||
// not an r-tree index. just return nil
|
||||
return nil
|
||||
}
|
||||
// execute the nearby search
|
||||
var min, max []float64
|
||||
if idx.rect != nil {
|
||||
min, max = idx.rect(bounds)
|
||||
}
|
||||
// set the center param to false, which uses the box dist calc.
|
||||
idx.rtr.KNN(&rect{min, max}, false, iter)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Intersects searches for rectangle items that intersect a target rect.
|
||||
// The specified index must have been created by AddIndex() and the target
|
||||
// is represented by the rect string. This string will be processed by the
|
||||
|
|
|
@ -1021,6 +1021,46 @@ func TestVariousTx(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNearby(t *testing.T) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
N := 100000
|
||||
db, _ := Open(":memory:")
|
||||
db.CreateSpatialIndex("points", "*", IndexRect)
|
||||
db.Update(func(tx *Tx) error {
|
||||
for i := 0; i < N; i++ {
|
||||
p := Point(
|
||||
rand.Float64()*100,
|
||||
rand.Float64()*100,
|
||||
rand.Float64()*100,
|
||||
rand.Float64()*100,
|
||||
)
|
||||
tx.Set(fmt.Sprintf("p:%d", i), p, nil)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
var keys, values []string
|
||||
var dists []float64
|
||||
var pdist float64
|
||||
var i int
|
||||
db.View(func(tx *Tx) error {
|
||||
tx.Nearby("points", Point(0, 0, 0, 0), func(key, value string, dist float64) bool {
|
||||
if i != 0 && dist < pdist {
|
||||
t.Fatal("out of order")
|
||||
}
|
||||
keys = append(keys, key)
|
||||
values = append(values, value)
|
||||
dists = append(dists, dist)
|
||||
pdist = dist
|
||||
i++
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
})
|
||||
if len(keys) != N {
|
||||
t.Fatalf("expected '%v', got '%v'", N, len(keys))
|
||||
}
|
||||
}
|
||||
|
||||
func Example_descKeys() {
|
||||
db, _ := Open(":memory:")
|
||||
db.CreateIndex("name", "*", IndexString)
|
||||
|
@ -2511,3 +2551,98 @@ func TestJSONIndex(t *testing.T) {
|
|||
t.Fatalf("expected %v, got %v", expect, strings.Join(keys, ","))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOnExpiredSync(t *testing.T) {
|
||||
db := testOpen(t)
|
||||
defer testClose(db)
|
||||
|
||||
var config Config
|
||||
if err := db.ReadConfig(&config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hits := make(chan int, 3)
|
||||
config.OnExpiredSync = func(key, value string, tx *Tx) error {
|
||||
n, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { hits <- n }()
|
||||
if n >= 2 {
|
||||
_, err = tx.Delete(key)
|
||||
if err != ErrNotFound {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
n++
|
||||
_, _, err = tx.Set(key, strconv.Itoa(n), &SetOptions{Expires: true, TTL: time.Millisecond * 100})
|
||||
return err
|
||||
}
|
||||
if err := db.SetConfig(config); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err := db.Update(func(tx *Tx) error {
|
||||
_, _, err := tx.Set("K", "0", &SetOptions{Expires: true, TTL: time.Millisecond * 100})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
ticks := time.NewTicker(time.Millisecond * 50)
|
||||
defer ticks.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ticks.C:
|
||||
err := db.View(func(tx *Tx) error {
|
||||
v, err := tx.Get("K", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n < 0 || n > 2 {
|
||||
t.Fail()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
OUTER1:
|
||||
for {
|
||||
select {
|
||||
case <-time.After(time.Second * 2):
|
||||
t.Fail()
|
||||
case v := <-hits:
|
||||
if v >= 2 {
|
||||
break OUTER1
|
||||
}
|
||||
}
|
||||
}
|
||||
err = db.View(func(tx *Tx) error {
|
||||
defer close(done)
|
||||
v, err := tx.Get("K")
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
return err
|
||||
}
|
||||
if v != "2" {
|
||||
t.Fail()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.6
|
||||
|
|
|
@ -14,6 +14,7 @@ Authors
|
|||
* 1995 Sphere volume fix for degeneracy problem submitted by Paul Brook
|
||||
* 2004 Templated C++ port by Greg Douglas
|
||||
* 2016 Go port by Josh Baker
|
||||
* 2018 Added kNN and merged in some of the RBush logic by Vladimir Agafonkin
|
||||
|
||||
License
|
||||
-------
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"github.com/tidwall/tinyqueue"
|
||||
)
|
||||
|
||||
type queueItem struct {
|
||||
node *treeNode
|
||||
isItem bool
|
||||
dist float64
|
||||
}
|
||||
|
||||
func (item *queueItem) Less(b tinyqueue.Item) bool {
|
||||
return item.dist < b.(*queueItem).dist
|
||||
}
|
||||
|
||||
// KNN returns items nearest to farthest. The dist param is the "box distance".
|
||||
func (tr *RTree) KNN(min, max []float64, center bool, iter func(item interface{}, dist float64) bool) bool {
|
||||
var isBox bool
|
||||
knnPoint := make([]float64, tr.dims)
|
||||
|
||||
bbox := &treeNode{min: min, max: max}
|
||||
|
||||
for i := 0; i < tr.dims; i++ {
|
||||
knnPoint[i] = (bbox.min[i] + bbox.max[i]) / 2
|
||||
if !isBox && bbox.min[i] != bbox.max[i] {
|
||||
isBox = true
|
||||
}
|
||||
}
|
||||
node := tr.data
|
||||
queue := tinyqueue.New(nil)
|
||||
for node != nil {
|
||||
for i := 0; i < node.count; i++ {
|
||||
child := node.children[i]
|
||||
var dist float64
|
||||
if isBox {
|
||||
dist = boxDistRect(bbox, child)
|
||||
} else {
|
||||
dist = boxDistPoint(knnPoint, child)
|
||||
}
|
||||
queue.Push(&queueItem{node: child, isItem: node.leaf, dist: dist})
|
||||
}
|
||||
for queue.Len() > 0 && queue.Peek().(*queueItem).isItem {
|
||||
item := queue.Pop().(*queueItem)
|
||||
if !iter(item.node.unsafeItem().item, item.dist) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
last := queue.Pop()
|
||||
if last != nil {
|
||||
node = (*treeNode)(last.(*queueItem).node)
|
||||
} else {
|
||||
node = nil
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func boxDistRect(a, b *treeNode) float64 {
|
||||
var dist float64
|
||||
for i := 0; i < len(a.min); i++ {
|
||||
var min, max float64
|
||||
if a.min[i] > b.min[i] {
|
||||
min = a.min[i]
|
||||
} else {
|
||||
min = b.min[i]
|
||||
}
|
||||
if a.max[i] < b.max[i] {
|
||||
max = a.max[i]
|
||||
} else {
|
||||
max = b.max[i]
|
||||
}
|
||||
squared := min - max
|
||||
if squared > 0 {
|
||||
dist += squared * squared
|
||||
}
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
func boxDistPoint(point []float64, childBox *treeNode) float64 {
|
||||
var dist float64
|
||||
for i := 0; i < len(point); i++ {
|
||||
d := axisDist(point[i], childBox.min[i], childBox.max[i])
|
||||
dist += d * d
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
func axisDist(k, min, max float64) float64 {
|
||||
if k < min {
|
||||
return min - k
|
||||
}
|
||||
if k <= max {
|
||||
return 0
|
||||
}
|
||||
return k - max
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package base
|
||||
|
||||
import "math"
|
||||
|
||||
// Load bulk load items into the R-tree.
|
||||
func (tr *RTree) Load(mins, maxs [][]float64, items []interface{}) {
|
||||
if len(items) < tr.minEntries {
|
||||
for i := 0; i < len(items); i++ {
|
||||
tr.Insert(mins[i], maxs[i], items[i])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// prefill the items
|
||||
fitems := make([]*treeNode, len(items))
|
||||
for i := 0; i < len(items); i++ {
|
||||
item := &treeItem{min: mins[i], max: maxs[i], item: items[i]}
|
||||
fitems[i] = item.unsafeNode()
|
||||
}
|
||||
|
||||
// following equations are defined in the paper describing OMT
|
||||
N := len(fitems)
|
||||
M := tr.maxEntries
|
||||
h := int(math.Ceil(math.Log(float64(N)) / math.Log(float64(M))))
|
||||
Nsubtree := int(math.Pow(float64(M), float64(h-1)))
|
||||
S := int(math.Ceil(math.Sqrt(float64(N) / float64(Nsubtree))))
|
||||
|
||||
// sort by the initial axis
|
||||
axis := 0
|
||||
sortByAxis(fitems, axis)
|
||||
|
||||
// build the root node. it's split differently from the subtrees.
|
||||
children := make([]*treeNode, 0, S)
|
||||
for i := 0; i < S; i++ {
|
||||
var part []*treeNode
|
||||
if i == S-1 {
|
||||
// last split
|
||||
part = fitems[len(fitems)/S*i:]
|
||||
} else {
|
||||
part = fitems[len(fitems)/S*i : len(fitems)/S*(i+1)]
|
||||
}
|
||||
children = append(children, tr.omt(part, h-1, axis+1))
|
||||
}
|
||||
|
||||
node := tr.createNode(children)
|
||||
node.leaf = false
|
||||
node.height = h
|
||||
tr.calcBBox(node)
|
||||
|
||||
if tr.data.count == 0 {
|
||||
// save as is if tree is empty
|
||||
tr.data = node
|
||||
} else if tr.data.height == node.height {
|
||||
// split root if trees have the same height
|
||||
tr.splitRoot(tr.data, node)
|
||||
} else {
|
||||
if tr.data.height < node.height {
|
||||
// swap trees if inserted one is bigger
|
||||
tr.data, node = node, tr.data
|
||||
}
|
||||
|
||||
// insert the small tree into the large tree at appropriate level
|
||||
tr.insert(node, nil, tr.data.height-node.height-1, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *RTree) omt(fitems []*treeNode, h, axis int) *treeNode {
|
||||
if len(fitems) <= tr.maxEntries {
|
||||
// reached leaf level; return leaf
|
||||
children := make([]*treeNode, len(fitems))
|
||||
copy(children, fitems)
|
||||
node := tr.createNode(children)
|
||||
node.height = h
|
||||
tr.calcBBox(node)
|
||||
return node
|
||||
}
|
||||
|
||||
// sort the items on a different axis than the previous level.
|
||||
sortByAxis(fitems, axis%tr.dims)
|
||||
children := make([]*treeNode, 0, tr.maxEntries)
|
||||
partsz := len(fitems) / tr.maxEntries
|
||||
for i := 0; i < tr.maxEntries; i++ {
|
||||
var part []*treeNode
|
||||
if i == tr.maxEntries-1 {
|
||||
// last part
|
||||
part = fitems[partsz*i:]
|
||||
} else {
|
||||
part = fitems[partsz*i : partsz*(i+1)]
|
||||
}
|
||||
children = append(children, tr.omt(part, h-1, axis+1))
|
||||
}
|
||||
node := tr.createNode(children)
|
||||
node.height = h
|
||||
node.leaf = false
|
||||
tr.calcBBox(node)
|
||||
return node
|
||||
}
|
|
@ -0,0 +1,673 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"math"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// precalculate infinity
|
||||
var mathInfNeg = math.Inf(-1)
|
||||
var mathInfPos = math.Inf(+1)
|
||||
|
||||
type treeNode struct {
|
||||
min, max []float64
|
||||
children []*treeNode
|
||||
count int
|
||||
height int
|
||||
leaf bool
|
||||
}
|
||||
|
||||
func (node *treeNode) unsafeItem() *treeItem {
|
||||
return (*treeItem)(unsafe.Pointer(node))
|
||||
}
|
||||
|
||||
func (tr *RTree) createNode(children []*treeNode) *treeNode {
|
||||
n := &treeNode{
|
||||
height: 1,
|
||||
leaf: true,
|
||||
children: make([]*treeNode, tr.maxEntries+1),
|
||||
}
|
||||
if len(children) > 0 {
|
||||
n.count = len(children)
|
||||
copy(n.children[:n.count], children)
|
||||
}
|
||||
n.min = make([]float64, tr.dims)
|
||||
n.max = make([]float64, tr.dims)
|
||||
for i := 0; i < tr.dims; i++ {
|
||||
n.min[i] = mathInfPos
|
||||
n.max[i] = mathInfNeg
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (node *treeNode) extend(b *treeNode) {
|
||||
for i := 0; i < len(node.min); i++ {
|
||||
if b.min[i] < node.min[i] {
|
||||
node.min[i] = b.min[i]
|
||||
}
|
||||
if b.max[i] > node.max[i] {
|
||||
node.max[i] = b.max[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (node *treeNode) area() float64 {
|
||||
area := node.max[0] - node.min[0]
|
||||
for i := 1; i < len(node.min); i++ {
|
||||
area *= node.max[i] - node.min[i]
|
||||
}
|
||||
return area
|
||||
}
|
||||
|
||||
func (node *treeNode) enlargedAreaAxis(b *treeNode, axis int) float64 {
|
||||
var max, min float64
|
||||
if b.max[axis] > node.max[axis] {
|
||||
max = b.max[axis]
|
||||
} else {
|
||||
max = node.max[axis]
|
||||
}
|
||||
if b.min[axis] < node.min[axis] {
|
||||
min = b.min[axis]
|
||||
} else {
|
||||
min = node.min[axis]
|
||||
}
|
||||
return max - min
|
||||
}
|
||||
|
||||
func (node *treeNode) enlargedArea(b *treeNode) float64 {
|
||||
area := node.enlargedAreaAxis(b, 0)
|
||||
for i := 1; i < len(node.min); i++ {
|
||||
area *= node.enlargedAreaAxis(b, i)
|
||||
}
|
||||
return area
|
||||
}
|
||||
|
||||
func (node *treeNode) intersectionAreaAxis(b *treeNode, axis int) float64 {
|
||||
var max, min float64
|
||||
if node.max[axis] < b.max[axis] {
|
||||
max = node.max[axis]
|
||||
} else {
|
||||
max = b.max[axis]
|
||||
}
|
||||
if node.min[axis] > b.min[axis] {
|
||||
min = node.min[axis]
|
||||
} else {
|
||||
min = b.min[axis]
|
||||
}
|
||||
if max > min {
|
||||
return max - min
|
||||
}
|
||||
return 0
|
||||
}
|
||||
func (node *treeNode) intersectionArea(b *treeNode) float64 {
|
||||
area := node.intersectionAreaAxis(b, 0)
|
||||
for i := 1; i < len(node.min); i++ {
|
||||
area *= node.intersectionAreaAxis(b, i)
|
||||
}
|
||||
return area
|
||||
}
|
||||
func (node *treeNode) margin() float64 {
|
||||
margin := node.max[0] - node.min[0]
|
||||
for i := 1; i < len(node.min); i++ {
|
||||
margin += node.max[i] - node.min[i]
|
||||
}
|
||||
return margin
|
||||
}
|
||||
|
||||
type result int
|
||||
|
||||
const (
|
||||
not result = 0
|
||||
intersects result = 1
|
||||
contains result = 2
|
||||
)
|
||||
|
||||
func (node *treeNode) overlaps(b *treeNode) result {
|
||||
for i := 0; i < len(node.min); i++ {
|
||||
if b.min[i] > node.max[i] || b.max[i] < node.min[i] {
|
||||
return not
|
||||
}
|
||||
if node.min[i] > b.min[i] || b.max[i] > node.max[i] {
|
||||
i++
|
||||
for ; i < len(node.min); i++ {
|
||||
if b.min[i] > node.max[i] || b.max[i] < node.min[i] {
|
||||
return not
|
||||
}
|
||||
}
|
||||
return intersects
|
||||
}
|
||||
}
|
||||
return contains
|
||||
}
|
||||
|
||||
func (node *treeNode) intersects(b *treeNode) bool {
|
||||
for i := 0; i < len(node.min); i++ {
|
||||
if b.min[i] > node.max[i] || b.max[i] < node.min[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (node *treeNode) findItem(item interface{}) int {
|
||||
for i := 0; i < node.count; i++ {
|
||||
if node.children[i].unsafeItem().item == item {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (node *treeNode) contains(b *treeNode) bool {
|
||||
for i := 0; i < len(node.min); i++ {
|
||||
if node.min[i] > b.min[i] || b.max[i] > node.max[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (node *treeNode) childCount() int {
|
||||
if node.leaf {
|
||||
return node.count
|
||||
}
|
||||
var n int
|
||||
for i := 0; i < node.count; i++ {
|
||||
n += node.children[i].childCount()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
type treeItem struct {
|
||||
min, max []float64
|
||||
item interface{}
|
||||
}
|
||||
|
||||
func (item *treeItem) unsafeNode() *treeNode {
|
||||
return (*treeNode)(unsafe.Pointer(item))
|
||||
}
|
||||
|
||||
// RTree is an R-tree
|
||||
type RTree struct {
|
||||
dims int
|
||||
maxEntries int
|
||||
minEntries int
|
||||
data *treeNode // root node
|
||||
// resusable fields, these help performance of common mutable operations.
|
||||
reuse struct {
|
||||
path []*treeNode // for reinsertion path
|
||||
indexes []int // for remove function
|
||||
stack []int // for bulk loading
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new R-tree
|
||||
func New(dims, maxEntries int) *RTree {
|
||||
if dims <= 0 {
|
||||
panic("invalid dimensions")
|
||||
}
|
||||
|
||||
tr := &RTree{}
|
||||
tr.dims = dims
|
||||
tr.maxEntries = int(math.Max(4, float64(maxEntries)))
|
||||
tr.minEntries = int(math.Max(2, math.Ceil(float64(tr.maxEntries)*0.4)))
|
||||
tr.data = tr.createNode(nil)
|
||||
return tr
|
||||
}
|
||||
|
||||
// Insert inserts an item
|
||||
func (tr *RTree) Insert(min, max []float64, item interface{}) {
|
||||
if len(min) != tr.dims || len(max) != tr.dims {
|
||||
panic("invalid dimensions")
|
||||
}
|
||||
if item == nil {
|
||||
panic("nil item")
|
||||
}
|
||||
bbox := treeNode{min: min, max: max}
|
||||
tr.insert(&bbox, item, tr.data.height-1, false)
|
||||
}
|
||||
|
||||
func (tr *RTree) insert(bbox *treeNode, item interface{}, level int, isNode bool) {
|
||||
tr.reuse.path = tr.reuse.path[:0]
|
||||
node, insertPath := tr.chooseSubtree(bbox, tr.data, level, tr.reuse.path)
|
||||
if item == nil {
|
||||
// item is only nil when bulk loading a node
|
||||
if node.leaf {
|
||||
panic("loading node into leaf")
|
||||
}
|
||||
node.children[node.count] = bbox
|
||||
node.count++
|
||||
} else {
|
||||
ti := &treeItem{min: bbox.min, max: bbox.max, item: item}
|
||||
node.children[node.count] = ti.unsafeNode()
|
||||
node.count++
|
||||
}
|
||||
node.extend(bbox)
|
||||
for level >= 0 {
|
||||
if insertPath[level].count > tr.maxEntries {
|
||||
insertPath = tr.split(insertPath, level)
|
||||
level--
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
tr.adjustParentBBoxes(bbox, insertPath, level)
|
||||
tr.reuse.path = insertPath
|
||||
}
|
||||
|
||||
func (tr *RTree) adjustParentBBoxes(bbox *treeNode, path []*treeNode, level int) {
|
||||
// adjust bboxes along the given tree path
|
||||
for i := level; i >= 0; i-- {
|
||||
path[i].extend(bbox)
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *RTree) chooseSubtree(bbox, node *treeNode, level int, path []*treeNode) (*treeNode, []*treeNode) {
|
||||
var targetNode *treeNode
|
||||
var area, enlargement, minArea, minEnlargement float64
|
||||
for {
|
||||
path = append(path, node)
|
||||
if node.leaf || len(path)-1 == level {
|
||||
break
|
||||
}
|
||||
minEnlargement = mathInfPos
|
||||
minArea = minEnlargement
|
||||
for i := 0; i < node.count; i++ {
|
||||
child := node.children[i]
|
||||
area = child.area()
|
||||
enlargement = bbox.enlargedArea(child) - area
|
||||
if enlargement < minEnlargement {
|
||||
minEnlargement = enlargement
|
||||
if area < minArea {
|
||||
minArea = area
|
||||
}
|
||||
targetNode = child
|
||||
} else if enlargement == minEnlargement {
|
||||
if area < minArea {
|
||||
minArea = area
|
||||
targetNode = child
|
||||
}
|
||||
}
|
||||
}
|
||||
if targetNode != nil {
|
||||
node = targetNode
|
||||
} else if node.count > 0 {
|
||||
node = (*treeNode)(node.children[0])
|
||||
} else {
|
||||
node = nil
|
||||
}
|
||||
}
|
||||
return node, path
|
||||
}
|
||||
func (tr *RTree) split(insertPath []*treeNode, level int) []*treeNode {
|
||||
var node = insertPath[level]
|
||||
var M = node.count
|
||||
var m = tr.minEntries
|
||||
|
||||
tr.chooseSplitAxis(node, m, M)
|
||||
splitIndex := tr.chooseSplitIndex(node, m, M)
|
||||
|
||||
spliced := make([]*treeNode, node.count-splitIndex)
|
||||
copy(spliced, node.children[splitIndex:])
|
||||
node.count = splitIndex
|
||||
|
||||
newNode := tr.createNode(spliced)
|
||||
newNode.height = node.height
|
||||
newNode.leaf = node.leaf
|
||||
|
||||
tr.calcBBox(node)
|
||||
tr.calcBBox(newNode)
|
||||
|
||||
if level != 0 {
|
||||
insertPath[level-1].children[insertPath[level-1].count] = newNode
|
||||
insertPath[level-1].count++
|
||||
} else {
|
||||
tr.splitRoot(node, newNode)
|
||||
}
|
||||
return insertPath
|
||||
}
|
||||
func (tr *RTree) chooseSplitIndex(node *treeNode, m, M int) int {
|
||||
var i int
|
||||
var bbox1, bbox2 *treeNode
|
||||
var overlap, area, minOverlap, minArea float64
|
||||
var index int
|
||||
|
||||
minArea = mathInfPos
|
||||
minOverlap = minArea
|
||||
|
||||
for i = m; i <= M-m; i++ {
|
||||
bbox1 = tr.distBBox(node, 0, i, nil)
|
||||
bbox2 = tr.distBBox(node, i, M, nil)
|
||||
|
||||
overlap = bbox1.intersectionArea(bbox2)
|
||||
area = bbox1.area() + bbox2.area()
|
||||
|
||||
// choose distribution with minimum overlap
|
||||
if overlap < minOverlap {
|
||||
minOverlap = overlap
|
||||
index = i
|
||||
|
||||
if area < minArea {
|
||||
minArea = area
|
||||
}
|
||||
} else if overlap == minOverlap {
|
||||
// otherwise choose distribution with minimum area
|
||||
if area < minArea {
|
||||
minArea = area
|
||||
index = i
|
||||
}
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
func (tr *RTree) calcBBox(node *treeNode) {
|
||||
tr.distBBox(node, 0, node.count, node)
|
||||
}
|
||||
func (tr *RTree) chooseSplitAxis(node *treeNode, m, M int) {
|
||||
minMargin := tr.allDistMargin(node, m, M, 0)
|
||||
var minAxis int
|
||||
for axis := 1; axis < tr.dims; axis++ {
|
||||
margin := tr.allDistMargin(node, m, M, axis)
|
||||
if margin < minMargin {
|
||||
minMargin = margin
|
||||
minAxis = axis
|
||||
}
|
||||
}
|
||||
if minAxis < tr.dims {
|
||||
tr.sortNodes(node, minAxis)
|
||||
}
|
||||
}
|
||||
func (tr *RTree) splitRoot(node, newNode *treeNode) {
|
||||
tr.data = tr.createNode([]*treeNode{node, newNode})
|
||||
tr.data.height = node.height + 1
|
||||
tr.data.leaf = false
|
||||
tr.calcBBox(tr.data)
|
||||
}
|
||||
func (tr *RTree) distBBox(node *treeNode, k, p int, destNode *treeNode) *treeNode {
|
||||
if destNode == nil {
|
||||
destNode = tr.createNode(nil)
|
||||
} else {
|
||||
for i := 0; i < tr.dims; i++ {
|
||||
destNode.min[i] = mathInfPos
|
||||
destNode.max[i] = mathInfNeg
|
||||
}
|
||||
}
|
||||
for i := k; i < p; i++ {
|
||||
if node.leaf {
|
||||
destNode.extend(node.children[i])
|
||||
} else {
|
||||
destNode.extend((*treeNode)(node.children[i]))
|
||||
}
|
||||
}
|
||||
return destNode
|
||||
}
|
||||
func (tr *RTree) allDistMargin(node *treeNode, m, M int, axis int) float64 {
|
||||
tr.sortNodes(node, axis)
|
||||
|
||||
var leftBBox = tr.distBBox(node, 0, m, nil)
|
||||
var rightBBox = tr.distBBox(node, M-m, M, nil)
|
||||
var margin = leftBBox.margin() + rightBBox.margin()
|
||||
|
||||
var i int
|
||||
|
||||
if node.leaf {
|
||||
for i = m; i < M-m; i++ {
|
||||
leftBBox.extend(node.children[i])
|
||||
margin += leftBBox.margin()
|
||||
}
|
||||
for i = M - m - 1; i >= m; i-- {
|
||||
leftBBox.extend(node.children[i])
|
||||
margin += rightBBox.margin()
|
||||
}
|
||||
} else {
|
||||
for i = m; i < M-m; i++ {
|
||||
child := (*treeNode)(node.children[i])
|
||||
leftBBox.extend(child)
|
||||
margin += leftBBox.margin()
|
||||
}
|
||||
for i = M - m - 1; i >= m; i-- {
|
||||
child := (*treeNode)(node.children[i])
|
||||
leftBBox.extend(child)
|
||||
margin += rightBBox.margin()
|
||||
}
|
||||
}
|
||||
return margin
|
||||
}
|
||||
func (tr *RTree) sortNodes(node *treeNode, axis int) {
|
||||
sortByAxis(node.children[:node.count], axis)
|
||||
}
|
||||
|
||||
func sortByAxis(items []*treeNode, axis int) {
|
||||
if len(items) < 2 {
|
||||
return
|
||||
}
|
||||
left, right := 0, len(items)-1
|
||||
pivotIndex := len(items) / 2
|
||||
items[pivotIndex], items[right] = items[right], items[pivotIndex]
|
||||
for i := range items {
|
||||
if items[i].min[axis] < items[right].min[axis] {
|
||||
items[i], items[left] = items[left], items[i]
|
||||
left++
|
||||
}
|
||||
}
|
||||
items[left], items[right] = items[right], items[left]
|
||||
sortByAxis(items[:left], axis)
|
||||
sortByAxis(items[left+1:], axis)
|
||||
}
|
||||
|
||||
// Search searches the tree for items in the input rectangle
|
||||
func (tr *RTree) Search(min, max []float64, iter func(item interface{}) bool) bool {
|
||||
bbox := &treeNode{min: min, max: max}
|
||||
if !tr.data.intersects(bbox) {
|
||||
return true
|
||||
}
|
||||
return tr.search(tr.data, bbox, iter)
|
||||
}
|
||||
|
||||
func (tr *RTree) search(node, bbox *treeNode, iter func(item interface{}) bool) bool {
|
||||
if node.leaf {
|
||||
for i := 0; i < node.count; i++ {
|
||||
if bbox.intersects(node.children[i]) {
|
||||
if !iter(node.children[i].unsafeItem().item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < node.count; i++ {
|
||||
r := bbox.overlaps(node.children[i])
|
||||
if r == intersects {
|
||||
if !tr.search(node.children[i], bbox, iter) {
|
||||
return false
|
||||
}
|
||||
} else if r == contains {
|
||||
if !scan(node.children[i], iter) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (tr *RTree) IsEmpty() bool {
|
||||
empty := true
|
||||
tr.Scan(func(item interface{}) bool {
|
||||
empty = false
|
||||
return false
|
||||
})
|
||||
return empty
|
||||
}
|
||||
|
||||
// Remove removes an item from the R-tree.
|
||||
func (tr *RTree) Remove(min, max []float64, item interface{}) {
|
||||
bbox := &treeNode{min: min, max: max}
|
||||
tr.remove(bbox, item)
|
||||
}
|
||||
|
||||
func (tr *RTree) remove(bbox *treeNode, item interface{}) {
|
||||
path := tr.reuse.path[:0]
|
||||
indexes := tr.reuse.indexes[:0]
|
||||
|
||||
var node = tr.data
|
||||
var i int
|
||||
var parent *treeNode
|
||||
var index int
|
||||
var goingUp bool
|
||||
|
||||
for node != nil || len(path) != 0 {
|
||||
if node == nil {
|
||||
node = path[len(path)-1]
|
||||
path = path[:len(path)-1]
|
||||
if len(path) == 0 {
|
||||
parent = nil
|
||||
} else {
|
||||
parent = path[len(path)-1]
|
||||
}
|
||||
i = indexes[len(indexes)-1]
|
||||
indexes = indexes[:len(indexes)-1]
|
||||
goingUp = true
|
||||
}
|
||||
|
||||
if node.leaf {
|
||||
index = node.findItem(item)
|
||||
if index != -1 {
|
||||
// item found, remove the item and condense tree upwards
|
||||
copy(node.children[index:], node.children[index+1:])
|
||||
node.children[node.count-1] = nil
|
||||
node.count--
|
||||
path = append(path, node)
|
||||
tr.condense(path)
|
||||
goto done
|
||||
}
|
||||
}
|
||||
if !goingUp && !node.leaf && node.contains(bbox) { // go down
|
||||
path = append(path, node)
|
||||
indexes = append(indexes, i)
|
||||
i = 0
|
||||
parent = node
|
||||
node = (*treeNode)(node.children[0])
|
||||
} else if parent != nil { // go right
|
||||
i++
|
||||
if i == parent.count {
|
||||
node = nil
|
||||
} else {
|
||||
node = (*treeNode)(parent.children[i])
|
||||
}
|
||||
goingUp = false
|
||||
} else {
|
||||
node = nil
|
||||
}
|
||||
}
|
||||
done:
|
||||
tr.reuse.path = path
|
||||
tr.reuse.indexes = indexes
|
||||
return
|
||||
}
|
||||
func (tr *RTree) condense(path []*treeNode) {
|
||||
// go through the path, removing empty nodes and updating bboxes
|
||||
var siblings []*treeNode
|
||||
for i := len(path) - 1; i >= 0; i-- {
|
||||
if path[i].count == 0 {
|
||||
if i > 0 {
|
||||
siblings = path[i-1].children[:path[i-1].count]
|
||||
index := -1
|
||||
for j := 0; j < len(siblings); j++ {
|
||||
if siblings[j] == path[i] {
|
||||
index = j
|
||||
break
|
||||
}
|
||||
}
|
||||
copy(siblings[index:], siblings[index+1:])
|
||||
siblings[len(siblings)-1] = nil
|
||||
path[i-1].count--
|
||||
//siblings = siblings[:len(siblings)-1]
|
||||
//path[i-1].children = siblings
|
||||
} else {
|
||||
tr.data = tr.createNode(nil) // clear tree
|
||||
}
|
||||
} else {
|
||||
tr.calcBBox(path[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count returns the number of items in the R-tree.
|
||||
func (tr *RTree) Count() int {
|
||||
return tr.data.childCount()
|
||||
}
|
||||
|
||||
// Traverse iterates over the entire R-tree and includes all nodes and items.
|
||||
func (tr *RTree) Traverse(iter func(min, max []float64, level int, item interface{}) bool) bool {
|
||||
return tr.traverse(tr.data, iter)
|
||||
}
|
||||
|
||||
func (tr *RTree) traverse(node *treeNode, iter func(min, max []float64, level int, item interface{}) bool) bool {
|
||||
if !iter(node.min, node.max, int(node.height), nil) {
|
||||
return false
|
||||
}
|
||||
if node.leaf {
|
||||
for i := 0; i < node.count; i++ {
|
||||
child := node.children[i]
|
||||
if !iter(child.min, child.max, 0, child.unsafeItem().item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < node.count; i++ {
|
||||
child := node.children[i]
|
||||
if !tr.traverse(child, iter) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Scan iterates over the entire R-tree
|
||||
func (tr *RTree) Scan(iter func(item interface{}) bool) bool {
|
||||
return scan(tr.data, iter)
|
||||
}
|
||||
|
||||
func scan(node *treeNode, iter func(item interface{}) bool) bool {
|
||||
if node.leaf {
|
||||
for i := 0; i < node.count; i++ {
|
||||
child := node.children[i]
|
||||
if !iter(child.unsafeItem().item) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := 0; i < node.count; i++ {
|
||||
child := node.children[i]
|
||||
if !scan(child, iter) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Bounds returns the bounding box of the entire R-tree
|
||||
func (tr *RTree) Bounds() (min, max []float64) {
|
||||
if tr.data.count > 0 {
|
||||
return tr.data.min, tr.data.max
|
||||
}
|
||||
return make([]float64, tr.dims), make([]float64, tr.dims)
|
||||
}
|
||||
|
||||
// Complexity returns the complexity of the R-tree. The higher the value, the
|
||||
// more complex the tree. The value of 1 is the lowest.
|
||||
func (tr *RTree) Complexity() float64 {
|
||||
var nodeCount int
|
||||
var itemCount int
|
||||
tr.Traverse(func(_, _ []float64, level int, _ interface{}) bool {
|
||||
if level == 0 {
|
||||
itemCount++
|
||||
} else {
|
||||
nodeCount++
|
||||
}
|
||||
return true
|
||||
})
|
||||
return float64(tr.maxEntries*nodeCount) / float64(itemCount)
|
||||
}
|
|
@ -0,0 +1,584 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const D = 2
|
||||
const M = 13
|
||||
|
||||
type Rect struct {
|
||||
min, max []float64
|
||||
item interface{}
|
||||
}
|
||||
|
||||
func (r *Rect) equals(r2 Rect) bool {
|
||||
if len(r.min) != len(r2.min) || len(r.max) != len(r2.max) || r.item != r2.item {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(r.min); i++ {
|
||||
if r.min[i] != r2.min[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(r.max); i++ {
|
||||
if r.max[i] != r2.max[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ptrMakePoint(vals ...float64) *Rect {
|
||||
var r Rect
|
||||
r.min = make([]float64, D)
|
||||
r.max = make([]float64, D)
|
||||
for i := 0; i < D && i < len(vals); i++ {
|
||||
r.min[i] = vals[i]
|
||||
r.max[i] = vals[i]
|
||||
}
|
||||
r.item = &r
|
||||
return &r
|
||||
}
|
||||
|
||||
func ptrMakeRect(vals ...float64) *Rect {
|
||||
var r Rect
|
||||
r.min = make([]float64, D)
|
||||
r.max = make([]float64, D)
|
||||
for i := 0; i < D && i < len(vals); i++ {
|
||||
r.min[i] = vals[i]
|
||||
r.max[i] = vals[i+D]
|
||||
}
|
||||
r.item = &r
|
||||
return &r
|
||||
}
|
||||
|
||||
func TestRTree(t *testing.T) {
|
||||
tr := New(D, M)
|
||||
p := ptrMakePoint(10, 10)
|
||||
tr.Insert(p.min, p.max, p.item)
|
||||
}
|
||||
|
||||
func TestPtrBasic2D(t *testing.T) {
|
||||
if D != 2 {
|
||||
return
|
||||
}
|
||||
tr := New(D, M)
|
||||
p1 := ptrMakePoint(-115, 33)
|
||||
p2 := ptrMakePoint(-113, 35)
|
||||
tr.Insert(p1.min, p1.max, p1.item)
|
||||
tr.Insert(p2.min, p2.max, p2.item)
|
||||
assertEqual(t, 2, tr.Count())
|
||||
|
||||
var points []*Rect
|
||||
bbox := ptrMakeRect(-116, 32, -114, 34)
|
||||
tr.Search(bbox.min, bbox.max, func(item interface{}) bool {
|
||||
points = append(points, item.(*Rect))
|
||||
return true
|
||||
})
|
||||
assertEqual(t, 1, len(points))
|
||||
tr.Remove(p1.min, p1.max, p1.item)
|
||||
assertEqual(t, 1, tr.Count())
|
||||
|
||||
points = nil
|
||||
bbox = ptrMakeRect(-116, 33, -114, 34)
|
||||
tr.Search(bbox.min, bbox.max, func(item interface{}) bool {
|
||||
points = append(points, item.(*Rect))
|
||||
return true
|
||||
})
|
||||
assertEqual(t, 0, len(points))
|
||||
tr.Remove(p2.min, p2.max, p2.item)
|
||||
assertEqual(t, 0, tr.Count())
|
||||
}
|
||||
|
||||
func getMemStats() runtime.MemStats {
|
||||
runtime.GC()
|
||||
time.Sleep(time.Millisecond)
|
||||
runtime.GC()
|
||||
var ms runtime.MemStats
|
||||
runtime.ReadMemStats(&ms)
|
||||
return ms
|
||||
}
|
||||
|
||||
func ptrMakeRandom(what string) *Rect {
|
||||
if what == "point" {
|
||||
vals := make([]float64, D)
|
||||
for i := 0; i < D; i++ {
|
||||
if i == 0 {
|
||||
vals[i] = rand.Float64()*360 - 180
|
||||
} else if i == 1 {
|
||||
vals[i] = rand.Float64()*180 - 90
|
||||
} else {
|
||||
vals[i] = rand.Float64()*100 - 50
|
||||
}
|
||||
}
|
||||
return ptrMakePoint(vals...)
|
||||
} else if what == "rect" {
|
||||
vals := make([]float64, D)
|
||||
for i := 0; i < D; i++ {
|
||||
if i == 0 {
|
||||
vals[i] = rand.Float64()*340 - 170
|
||||
} else if i == 1 {
|
||||
vals[i] = rand.Float64()*160 - 80
|
||||
} else {
|
||||
vals[i] = rand.Float64()*80 - 30
|
||||
}
|
||||
}
|
||||
rvals := make([]float64, D*2)
|
||||
for i := 0; i < D; i++ {
|
||||
rvals[i] = vals[i] - rand.Float64()*10
|
||||
rvals[D+i] = vals[i] + rand.Float64()*10
|
||||
}
|
||||
return ptrMakeRect(rvals...)
|
||||
}
|
||||
panic("??")
|
||||
}
|
||||
|
||||
func TestPtrRandom(t *testing.T) {
|
||||
t.Run(fmt.Sprintf("%dD", D), func(t *testing.T) {
|
||||
t.Run("point", func(t *testing.T) { ptrTestRandom(t, "point", 10000) })
|
||||
t.Run("rect", func(t *testing.T) { ptrTestRandom(t, "rect", 10000) })
|
||||
})
|
||||
}
|
||||
|
||||
func ptrTestRandom(t *testing.T, which string, n int) {
|
||||
fmt.Println("-------------------------------------------------")
|
||||
fmt.Printf("Testing Random %dD %ss\n", D, which)
|
||||
fmt.Println("-------------------------------------------------")
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
tr := New(D, M)
|
||||
min, max := tr.Bounds()
|
||||
assertEqual(t, make([]float64, D), min[:])
|
||||
assertEqual(t, make([]float64, D), max[:])
|
||||
|
||||
// create random objects
|
||||
m1 := getMemStats()
|
||||
objs := make([]*Rect, n)
|
||||
for i := 0; i < n; i++ {
|
||||
objs[i] = ptrMakeRandom(which)
|
||||
}
|
||||
|
||||
// insert the objects into tree
|
||||
m2 := getMemStats()
|
||||
start := time.Now()
|
||||
for _, r := range objs {
|
||||
tr.Insert(r.min, r.max, r.item)
|
||||
}
|
||||
durInsert := time.Since(start)
|
||||
m3 := getMemStats()
|
||||
assertEqual(t, len(objs), tr.Count())
|
||||
fmt.Printf("Inserted %d random %ss in %dms -- %d ops/sec\n",
|
||||
len(objs), which, int(durInsert.Seconds()*1000),
|
||||
int(float64(len(objs))/durInsert.Seconds()))
|
||||
fmt.Printf(" total cost is %d bytes/%s\n", int(m3.HeapAlloc-m1.HeapAlloc)/len(objs), which)
|
||||
fmt.Printf(" tree cost is %d bytes/%s\n", int(m3.HeapAlloc-m2.HeapAlloc)/len(objs), which)
|
||||
fmt.Printf(" tree overhead %d%%\n", int((float64(m3.HeapAlloc-m2.HeapAlloc)/float64(len(objs)))/(float64(m3.HeapAlloc-m1.HeapAlloc)/float64(len(objs)))*100))
|
||||
fmt.Printf(" complexity %f\n", tr.Complexity())
|
||||
|
||||
start = time.Now()
|
||||
// count all nodes and leaves
|
||||
var nodes int
|
||||
var leaves int
|
||||
var maxLevel int
|
||||
tr.Traverse(func(min, max []float64, level int, item interface{}) bool {
|
||||
if level != 0 {
|
||||
nodes++
|
||||
}
|
||||
if level == 1 {
|
||||
leaves++
|
||||
}
|
||||
if level > maxLevel {
|
||||
maxLevel = level
|
||||
}
|
||||
return true
|
||||
})
|
||||
fmt.Printf(" nodes: %d, leaves: %d, level: %d\n", nodes, leaves, maxLevel)
|
||||
|
||||
// verify mbr
|
||||
for i := 0; i < D; i++ {
|
||||
min[i] = math.Inf(+1)
|
||||
max[i] = math.Inf(-1)
|
||||
}
|
||||
for _, o := range objs {
|
||||
for i := 0; i < D; i++ {
|
||||
if o.min[i] < min[i] {
|
||||
min[i] = o.min[i]
|
||||
}
|
||||
if o.max[i] > max[i] {
|
||||
max[i] = o.max[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
minb, maxb := tr.Bounds()
|
||||
assertEqual(t, min, minb)
|
||||
assertEqual(t, max, maxb)
|
||||
|
||||
// scan
|
||||
var arr []*Rect
|
||||
tr.Scan(func(item interface{}) bool {
|
||||
arr = append(arr, item.(*Rect))
|
||||
return true
|
||||
})
|
||||
assertEqual(t, true, ptrTestHasSameItems(objs, arr))
|
||||
|
||||
// search
|
||||
ptrTestSearch(t, tr, objs, 0.10, true)
|
||||
ptrTestSearch(t, tr, objs, 0.50, true)
|
||||
ptrTestSearch(t, tr, objs, 1.00, true)
|
||||
|
||||
// knn
|
||||
ptrTestKNN(t, tr, objs, int(float64(len(objs))*0.01), true)
|
||||
ptrTestKNN(t, tr, objs, int(float64(len(objs))*0.50), true)
|
||||
ptrTestKNN(t, tr, objs, int(float64(len(objs))*1.00), true)
|
||||
|
||||
// remove all objects
|
||||
indexes := rand.Perm(len(objs))
|
||||
start = time.Now()
|
||||
for _, i := range indexes {
|
||||
tr.Remove(objs[i].min, objs[i].max, objs[i].item)
|
||||
}
|
||||
durRemove := time.Since(start)
|
||||
assertEqual(t, 0, tr.Count())
|
||||
fmt.Printf("Removed %d random %ss in %dms -- %d ops/sec\n",
|
||||
len(objs), which, int(durRemove.Seconds()*1000),
|
||||
int(float64(len(objs))/durRemove.Seconds()))
|
||||
|
||||
min, max = tr.Bounds()
|
||||
assertEqual(t, make([]float64, D), min[:])
|
||||
assertEqual(t, make([]float64, D), max[:])
|
||||
}
|
||||
|
||||
func ptrTestHasSameItems(a1, a2 []*Rect) bool {
|
||||
if len(a1) != len(a2) {
|
||||
return false
|
||||
}
|
||||
for _, p1 := range a1 {
|
||||
var found bool
|
||||
for _, p2 := range a2 {
|
||||
if p1.equals(*p2) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ptrTestSearch(t *testing.T, tr *RTree, objs []*Rect, percent float64, check bool) {
|
||||
var found int
|
||||
var start time.Time
|
||||
var stop time.Time
|
||||
defer func() {
|
||||
dur := stop.Sub(start)
|
||||
fmt.Printf("Searched %.0f%% (%d/%d items) in %dms -- %d ops/sec\n",
|
||||
percent*100, found, len(objs), int(dur.Seconds()*1000),
|
||||
int(float64(1)/dur.Seconds()),
|
||||
)
|
||||
}()
|
||||
min, max := tr.Bounds()
|
||||
vals := make([]float64, D*2)
|
||||
for i := 0; i < D; i++ {
|
||||
vals[i] = ((max[i]+min[i])/2 - ((max[i]-min[i])*percent)/2)
|
||||
vals[D+i] = ((max[i]+min[i])/2 + ((max[i]-min[i])*percent)/2)
|
||||
}
|
||||
var arr1 []*Rect
|
||||
var box *Rect
|
||||
if percent == 1 {
|
||||
box = ptrMakeRect(append(append([]float64{}, min[:]...), max[:]...)...)
|
||||
} else {
|
||||
box = ptrMakeRect(vals...)
|
||||
}
|
||||
start = time.Now()
|
||||
tr.Search(box.min, box.max, func(item interface{}) bool {
|
||||
if check {
|
||||
arr1 = append(arr1, item.(*Rect))
|
||||
}
|
||||
found++
|
||||
return true
|
||||
})
|
||||
stop = time.Now()
|
||||
if !check {
|
||||
return
|
||||
}
|
||||
var arr2 []*Rect
|
||||
for _, obj := range objs {
|
||||
if ptrTestIntersects(obj, box) {
|
||||
arr2 = append(arr2, obj)
|
||||
}
|
||||
}
|
||||
assertEqual(t, len(arr1), len(arr2))
|
||||
for _, o1 := range arr1 {
|
||||
var found bool
|
||||
for _, o2 := range arr2 {
|
||||
if o2.equals(*o1) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ptrTestKNN(t *testing.T, tr *RTree, objs []*Rect, n int, check bool) {
|
||||
var start time.Time
|
||||
var stop time.Time
|
||||
defer func() {
|
||||
dur := stop.Sub(start)
|
||||
fmt.Printf("KNN %d items in %dms -- %d ops/sec\n",
|
||||
n, int(dur.Seconds()*1000),
|
||||
int(float64(1)/dur.Seconds()),
|
||||
)
|
||||
}()
|
||||
min, max := tr.Bounds()
|
||||
pvals := make([]float64, D)
|
||||
for i := 0; i < D; i++ {
|
||||
pvals[i] = (max[i] + min[i]) / 2
|
||||
}
|
||||
point := ptrMakePoint(pvals...)
|
||||
|
||||
// gather the results, make sure that is matches exactly
|
||||
var arr1 []Rect
|
||||
var dists1 []float64
|
||||
pdist := math.Inf(-1)
|
||||
start = time.Now()
|
||||
tr.KNN(point.min, point.max, false, func(item interface{}, dist float64) bool {
|
||||
if len(arr1) == n {
|
||||
return false
|
||||
}
|
||||
arr1 = append(arr1, Rect{min: min, max: max, item: item})
|
||||
dists1 = append(dists1, dist)
|
||||
if dist < pdist {
|
||||
panic("dist out of order")
|
||||
}
|
||||
pdist = dist
|
||||
return true
|
||||
})
|
||||
stop = time.Now()
|
||||
assertEqual(t, true, n > len(objs) || n == len(arr1))
|
||||
|
||||
// get the KNN for the original array
|
||||
nobjs := make([]*Rect, len(objs))
|
||||
copy(nobjs, objs)
|
||||
sort.Slice(nobjs, func(i, j int) bool {
|
||||
idist := ptrTestBoxDist(pvals, nobjs[i].min, nobjs[i].max)
|
||||
jdist := ptrTestBoxDist(pvals, nobjs[j].min, nobjs[j].max)
|
||||
return idist < jdist
|
||||
})
|
||||
arr2 := nobjs[:len(arr1)]
|
||||
var dists2 []float64
|
||||
for i := 0; i < len(arr2); i++ {
|
||||
dist := ptrTestBoxDist(pvals, arr2[i].min, arr2[i].max)
|
||||
dists2 = append(dists2, dist)
|
||||
}
|
||||
// only compare the distances, not the objects because rectangles with
|
||||
// a dist of zero will not be ordered.
|
||||
assertEqual(t, dists1, dists2)
|
||||
|
||||
}
|
||||
|
||||
func ptrTestBoxDist(point []float64, min, max []float64) float64 {
|
||||
var dist float64
|
||||
for i := 0; i < len(point); i++ {
|
||||
d := ptrTestAxisDist(point[i], min[i], max[i])
|
||||
dist += d * d
|
||||
}
|
||||
return dist
|
||||
}
|
||||
func ptrTestAxisDist(k, min, max float64) float64 {
|
||||
if k < min {
|
||||
return min - k
|
||||
}
|
||||
if k <= max {
|
||||
return 0
|
||||
}
|
||||
return k - max
|
||||
}
|
||||
func ptrTestIntersects(obj, box *Rect) bool {
|
||||
for i := 0; i < D; i++ {
|
||||
if box.min[i] > obj.max[i] || box.max[i] < obj.min[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// func TestPtrInsertFlatPNG2D(t *testing.T) {
|
||||
// fmt.Println("-------------------------------------------------")
|
||||
// fmt.Println("Generating Cities PNG 2D (flat-insert-2d.png)")
|
||||
// fmt.Println("-------------------------------------------------")
|
||||
// tr := New()
|
||||
// var items []*Rect
|
||||
// c := cities.Cities
|
||||
// for i := 0; i < len(c); i++ {
|
||||
// x := c[i].Longitude
|
||||
// y := c[i].Latitude
|
||||
// items = append(items, ptrMakePoint(x, y))
|
||||
// }
|
||||
// start := time.Now()
|
||||
// for _, item := range items {
|
||||
// tr.Insert(item.min, item.max, item.item)
|
||||
// }
|
||||
// dur := time.Since(start)
|
||||
// fmt.Printf("wrote %d cities (flat) in %s (%.0f/ops)\n", len(c), dur, float64(len(c))/dur.Seconds())
|
||||
// withGIF := os.Getenv("GIFOUTPUT") != ""
|
||||
// if err := tr.SavePNG("ptr-flat-insert-2d.png", 1000, 1000, 1.25/360.0, 0, true, withGIF, os.Stdout); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// if !withGIF {
|
||||
// fmt.Println("use GIFOUTPUT=1 for animated gif")
|
||||
// }
|
||||
// }
|
||||
|
||||
// func TestPtrLoadFlatPNG2D(t *testing.T) {
|
||||
// fmt.Println("-------------------------------------------------")
|
||||
// fmt.Println("Generating Cities 2D PNG (flat-load-2d.png)")
|
||||
// fmt.Println("-------------------------------------------------")
|
||||
// tr := New()
|
||||
// var items []*Rect
|
||||
// c := cities.Cities
|
||||
// for i := 0; i < len(c); i++ {
|
||||
// x := c[i].Longitude
|
||||
// y := c[i].Latitude
|
||||
// items = append(items, ptrMakePoint(x, y))
|
||||
// }
|
||||
|
||||
// var mins [][D]float64
|
||||
// var maxs [][D]float64
|
||||
// var ifs []interface{}
|
||||
// for i := 0; i < len(items); i++ {
|
||||
// mins = append(mins, items[i].min)
|
||||
// maxs = append(maxs, items[i].max)
|
||||
// ifs = append(ifs, items[i].item)
|
||||
// }
|
||||
|
||||
// start := time.Now()
|
||||
// tr.Load(mins, maxs, ifs)
|
||||
// dur := time.Since(start)
|
||||
|
||||
// if true {
|
||||
// var all []*Rect
|
||||
// tr.Scan(func(min, max [D]float64, item interface{}) bool {
|
||||
// all = append(all, &Rect{min: min, max: max, item: item})
|
||||
// return true
|
||||
// })
|
||||
// assertEqual(t, len(all), len(items))
|
||||
|
||||
// for len(all) > 0 {
|
||||
// item := all[0]
|
||||
// var found bool
|
||||
// for _, city := range items {
|
||||
// if *city == *item {
|
||||
// found = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if !found {
|
||||
// t.Fatal("item not found")
|
||||
// }
|
||||
// all = all[1:]
|
||||
// }
|
||||
// }
|
||||
// fmt.Printf("wrote %d cities (flat) in %s (%.0f/ops)\n", len(c), dur, float64(len(c))/dur.Seconds())
|
||||
// withGIF := os.Getenv("GIFOUTPUT") != ""
|
||||
// if err := tr.SavePNG("ptr-flat-load-2d.png", 1000, 1000, 1.25/360.0, 0, true, withGIF, os.Stdout); err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// if !withGIF {
|
||||
// fmt.Println("use GIFOUTPUT=1 for animated gif")
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestBenchmarks(t *testing.T) {
|
||||
var points []*Rect
|
||||
for i := 0; i < 2000000; i++ {
|
||||
x := rand.Float64()*360 - 180
|
||||
y := rand.Float64()*180 - 90
|
||||
points = append(points, ptrMakePoint(x, y))
|
||||
}
|
||||
tr := New(D, M)
|
||||
start := time.Now()
|
||||
for i := len(points) / 2; i < len(points); i++ {
|
||||
tr.Insert(points[i].min, points[i].max, points[i].item)
|
||||
}
|
||||
dur := time.Since(start)
|
||||
log.Printf("insert 1M items one by one: %.3fs", dur.Seconds())
|
||||
////
|
||||
rarr := rand.Perm(len(points) / 2)
|
||||
start = time.Now()
|
||||
for i := 0; i < len(points)/2; i++ {
|
||||
a := points[rarr[i]+len(points)/2]
|
||||
b := points[rarr[i]]
|
||||
tr.Remove(a.min, a.max, a.item)
|
||||
tr.Insert(b.min, b.max, b.item)
|
||||
}
|
||||
dur = time.Since(start)
|
||||
log.Printf("replaced 1M items one by one: %.3fs", dur.Seconds())
|
||||
points = points[:len(points)/2]
|
||||
////
|
||||
start = time.Now()
|
||||
for i := 0; i < 1000; i++ {
|
||||
tr.Remove(points[i].min, points[i].max, points[i].item)
|
||||
}
|
||||
dur = time.Since(start)
|
||||
log.Printf("remove 100 items one by one: %.3fs", dur.Seconds())
|
||||
////
|
||||
bbox := ptrMakeRect(0, 0, 0+(360*0.0001), 0+(180*0.0001))
|
||||
start = time.Now()
|
||||
for i := 0; i < 1000; i++ {
|
||||
tr.Search(bbox.min, bbox.max, func(_ interface{}) bool { return true })
|
||||
}
|
||||
dur = time.Since(start)
|
||||
log.Printf("1000 searches of 0.01%% area: %.3fs", dur.Seconds())
|
||||
////
|
||||
bbox = ptrMakeRect(0, 0, 0+(360*0.01), 0+(180*0.01))
|
||||
start = time.Now()
|
||||
for i := 0; i < 1000; i++ {
|
||||
tr.Search(bbox.min, bbox.max, func(_ interface{}) bool { return true })
|
||||
}
|
||||
dur = time.Since(start)
|
||||
log.Printf("1000 searches of 1%% area: %.3fs", dur.Seconds())
|
||||
////
|
||||
bbox = ptrMakeRect(0, 0, 0+(360*0.10), 0+(180*0.10))
|
||||
start = time.Now()
|
||||
for i := 0; i < 1000; i++ {
|
||||
tr.Search(bbox.min, bbox.max, func(_ interface{}) bool { return true })
|
||||
}
|
||||
dur = time.Since(start)
|
||||
log.Printf("1000 searches of 10%% area: %.3fs", dur.Seconds())
|
||||
///
|
||||
|
||||
var mins [][]float64
|
||||
var maxs [][]float64
|
||||
var items []interface{}
|
||||
for i := 0; i < len(points); i++ {
|
||||
mins = append(mins, points[i].min)
|
||||
maxs = append(maxs, points[i].max)
|
||||
items = append(items, points[i].item)
|
||||
}
|
||||
|
||||
tr = New(D, M)
|
||||
start = time.Now()
|
||||
tr.Load(mins, maxs, items)
|
||||
dur = time.Since(start)
|
||||
log.Printf("bulk-insert 1M items: %.3fs", dur.Seconds())
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, expected, actual interface{}) {
|
||||
t.Helper()
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
t.Fatalf("expected '%v', got '%v'", expected, actual)
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var dims int
|
||||
var debug bool
|
||||
flag.IntVar(&dims, "dims", 4, "number of dimensions")
|
||||
flag.BoolVar(&debug, "debug", false, "turn on debug tracing")
|
||||
flag.Parse()
|
||||
// process rtree.go
|
||||
data, err := ioutil.ReadFile("src/rtree.go")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
data = []byte(strings.Replace(string(data), "// +build ignore", "// generated; DO NOT EDIT!", -1))
|
||||
if debug {
|
||||
data = []byte(strings.Replace(string(data), "TDEBUG", "true", -1))
|
||||
} else {
|
||||
data = []byte(strings.Replace(string(data), "TDEBUG", "false", -1))
|
||||
}
|
||||
var dimouts = make([]string, dims)
|
||||
var output string
|
||||
var recording bool
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "//") {
|
||||
idx := strings.Index(line, "//")
|
||||
switch strings.ToUpper(strings.TrimSpace(line[idx+2:])) {
|
||||
case "BEGIN":
|
||||
recording = true
|
||||
for i := 0; i < len(dimouts); i++ {
|
||||
dimouts[i] = ""
|
||||
}
|
||||
continue
|
||||
case "END":
|
||||
for _, out := range dimouts {
|
||||
if out != "" {
|
||||
output += out
|
||||
}
|
||||
}
|
||||
recording = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
if recording {
|
||||
for i := 0; i < len(dimouts); i++ {
|
||||
dimouts[i] += strings.Replace(line, "TNUMDIMS", strconv.FormatInt(int64(i+1), 10), -1) + "\n"
|
||||
}
|
||||
} else {
|
||||
output += line + "\n"
|
||||
}
|
||||
}
|
||||
// process rtree_base.go
|
||||
if err := os.RemoveAll("../dims"); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for i := 0; i < dims; i++ {
|
||||
sdim := strconv.FormatInt(int64(i+1), 10)
|
||||
data, err := ioutil.ReadFile("src/rtree_base.go")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
data = []byte(strings.Split(string(data), "// FILE_START")[1])
|
||||
if debug {
|
||||
data = []byte(strings.Replace(string(data), "TDEBUG", "true", -1))
|
||||
} else {
|
||||
data = []byte(strings.Replace(string(data), "TDEBUG", "false", -1))
|
||||
}
|
||||
data = []byte(strings.Replace(string(data), "TNUMDIMS", strconv.FormatInt(int64(i+1), 10), -1))
|
||||
data = []byte(strings.Replace(string(data), "DD_", "d"+strconv.FormatInt(int64(i+1), 10), -1))
|
||||
if err := os.MkdirAll("../dims/d"+sdim, 0777); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
output = string(append([]byte(output), data...))
|
||||
}
|
||||
if err := ioutil.WriteFile("../rtree.go", []byte(output), 0666); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
cd $(dirname "${BASH_SOURCE[0]}")
|
||||
|
||||
go run gen.go --dims=20 --debug=false
|
||||
cd ..
|
||||
go fmt
|
|
@ -1,134 +0,0 @@
|
|||
// +build ignore
|
||||
|
||||
package rtree
|
||||
|
||||
import "math"
|
||||
|
||||
type Iterator func(item Item) bool
|
||||
type Item interface {
|
||||
Rect(ctx interface{}) (min []float64, max []float64)
|
||||
}
|
||||
|
||||
type RTree struct {
|
||||
ctx interface{}
|
||||
// BEGIN
|
||||
trTNUMDIMS *dTNUMDIMSRTree
|
||||
// END
|
||||
}
|
||||
|
||||
func New(ctx interface{}) *RTree {
|
||||
return &RTree{
|
||||
ctx: ctx,
|
||||
// BEGIN
|
||||
trTNUMDIMS: dTNUMDIMSNew(),
|
||||
// END
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *RTree) Insert(item Item) {
|
||||
if item == nil {
|
||||
panic("nil item being added to RTree")
|
||||
}
|
||||
min, max := item.Rect(tr.ctx)
|
||||
if len(min) != len(max) {
|
||||
return // just return
|
||||
panic("invalid item rectangle")
|
||||
}
|
||||
switch len(min) {
|
||||
default:
|
||||
return // just return
|
||||
panic("invalid dimension")
|
||||
// BEGIN
|
||||
case TNUMDIMS:
|
||||
var amin, amax [TNUMDIMS]float64
|
||||
for i := 0; i < len(min); i++ {
|
||||
amin[i], amax[i] = min[i], max[i]
|
||||
}
|
||||
tr.trTNUMDIMS.Insert(amin, amax, item)
|
||||
// END
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *RTree) Remove(item Item) {
|
||||
if item == nil {
|
||||
panic("nil item being added to RTree")
|
||||
}
|
||||
min, max := item.Rect(tr.ctx)
|
||||
if len(min) != len(max) {
|
||||
return // just return
|
||||
panic("invalid item rectangle")
|
||||
}
|
||||
switch len(min) {
|
||||
default:
|
||||
return // just return
|
||||
panic("invalid dimension")
|
||||
// BEGIN
|
||||
case TNUMDIMS:
|
||||
var amin, amax [TNUMDIMS]float64
|
||||
for i := 0; i < len(min); i++ {
|
||||
amin[i], amax[i] = min[i], max[i]
|
||||
}
|
||||
tr.trTNUMDIMS.Remove(amin, amax, item)
|
||||
// END
|
||||
}
|
||||
}
|
||||
func (tr *RTree) Reset() {
|
||||
// BEGIN
|
||||
tr.trTNUMDIMS = dTNUMDIMSNew()
|
||||
// END
|
||||
}
|
||||
func (tr *RTree) Count() int {
|
||||
count := 0
|
||||
// BEGIN
|
||||
count += tr.trTNUMDIMS.Count()
|
||||
// END
|
||||
return count
|
||||
}
|
||||
func (tr *RTree) Search(bounds Item, iter Iterator) {
|
||||
if bounds == nil {
|
||||
panic("nil bounds being used for search")
|
||||
}
|
||||
min, max := bounds.Rect(tr.ctx)
|
||||
if len(min) != len(max) {
|
||||
return // just return
|
||||
panic("invalid item rectangle")
|
||||
}
|
||||
switch len(min) {
|
||||
default:
|
||||
return // just return
|
||||
panic("invalid dimension")
|
||||
// BEGIN
|
||||
case TNUMDIMS:
|
||||
// END
|
||||
}
|
||||
// BEGIN
|
||||
if !tr.searchTNUMDIMS(min, max, iter) {
|
||||
return
|
||||
}
|
||||
// END
|
||||
}
|
||||
|
||||
// BEGIN
|
||||
func (tr *RTree) searchTNUMDIMS(min, max []float64, iter Iterator) bool {
|
||||
var amin, amax [TNUMDIMS]float64
|
||||
for i := 0; i < TNUMDIMS; i++ {
|
||||
if i < len(min) {
|
||||
amin[i] = min[i]
|
||||
amax[i] = max[i]
|
||||
} else {
|
||||
amin[i] = math.Inf(-1)
|
||||
amax[i] = math.Inf(+1)
|
||||
}
|
||||
}
|
||||
ended := false
|
||||
tr.trTNUMDIMS.Search(amin, amax, func(dataID interface{}) bool {
|
||||
if !iter(dataID.(Item)) {
|
||||
ended = true
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
return !ended
|
||||
}
|
||||
|
||||
// END
|
|
@ -1,687 +0,0 @@
|
|||
// +build ignore
|
||||
|
||||
/*
|
||||
|
||||
TITLE
|
||||
|
||||
R-TREES: A DYNAMIC INDEX STRUCTURE FOR SPATIAL SEARCHING
|
||||
|
||||
DESCRIPTION
|
||||
|
||||
A Go version of the RTree algorithm.
|
||||
|
||||
AUTHORS
|
||||
|
||||
* 1983 Original algorithm and test code by Antonin Guttman and Michael Stonebraker, UC Berkely
|
||||
* 1994 ANCI C ported from original test code by Melinda Green - melinda@superliminal.com
|
||||
* 1995 Sphere volume fix for degeneracy problem submitted by Paul Brook
|
||||
* 2004 Templated C++ port by Greg Douglas
|
||||
* 2016 Go port by Josh Baker
|
||||
|
||||
LICENSE:
|
||||
|
||||
Entirely free for all uses. Enjoy!
|
||||
|
||||
*/
|
||||
|
||||
// Implementation of RTree, a multidimensional bounding rectangle tree.
|
||||
package rtree
|
||||
|
||||
import "math"
|
||||
|
||||
// FILE_START
|
||||
|
||||
func DD_fmin(a, b float64) float64 {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
func DD_fmax(a, b float64) float64 {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
const (
|
||||
DD_numDims = TNUMDIMS
|
||||
DD_maxNodes = 8
|
||||
DD_minNodes = DD_maxNodes / 2
|
||||
DD_useSphericalVolume = true // Better split classification, may be slower on some systems
|
||||
)
|
||||
|
||||
var DD_unitSphereVolume = []float64{
|
||||
0.000000, 2.000000, 3.141593, // Dimension 0,1,2
|
||||
4.188790, 4.934802, 5.263789, // Dimension 3,4,5
|
||||
5.167713, 4.724766, 4.058712, // Dimension 6,7,8
|
||||
3.298509, 2.550164, 1.884104, // Dimension 9,10,11
|
||||
1.335263, 0.910629, 0.599265, // Dimension 12,13,14
|
||||
0.381443, 0.235331, 0.140981, // Dimension 15,16,17
|
||||
0.082146, 0.046622, 0.025807, // Dimension 18,19,20
|
||||
}[DD_numDims]
|
||||
|
||||
type DD_RTree struct {
|
||||
root *DD_nodeT ///< Root of tree
|
||||
}
|
||||
|
||||
/// Minimal bounding rectangle (n-dimensional)
|
||||
type DD_rectT struct {
|
||||
min [DD_numDims]float64 ///< Min dimensions of bounding box
|
||||
max [DD_numDims]float64 ///< Max dimensions of bounding box
|
||||
}
|
||||
|
||||
/// May be data or may be another subtree
|
||||
/// The parents level determines this.
|
||||
/// If the parents level is 0, then this is data
|
||||
type DD_branchT struct {
|
||||
rect DD_rectT ///< Bounds
|
||||
child *DD_nodeT ///< Child node
|
||||
data interface{} ///< Data Id or Ptr
|
||||
}
|
||||
|
||||
/// DD_nodeT for each branch level
|
||||
type DD_nodeT struct {
|
||||
count int ///< Count
|
||||
level int ///< Leaf is zero, others positive
|
||||
branch [DD_maxNodes]DD_branchT ///< Branch
|
||||
}
|
||||
|
||||
func (node *DD_nodeT) isInternalNode() bool {
|
||||
return (node.level > 0) // Not a leaf, but a internal node
|
||||
}
|
||||
func (node *DD_nodeT) isLeaf() bool {
|
||||
return (node.level == 0) // A leaf, contains data
|
||||
}
|
||||
|
||||
/// A link list of nodes for reinsertion after a delete operation
|
||||
type DD_listNodeT struct {
|
||||
next *DD_listNodeT ///< Next in list
|
||||
node *DD_nodeT ///< Node
|
||||
}
|
||||
|
||||
const DD_notTaken = -1 // indicates that position
|
||||
|
||||
/// Variables for finding a split partition
|
||||
type DD_partitionVarsT struct {
|
||||
partition [DD_maxNodes + 1]int
|
||||
total int
|
||||
minFill int
|
||||
count [2]int
|
||||
cover [2]DD_rectT
|
||||
area [2]float64
|
||||
|
||||
branchBuf [DD_maxNodes + 1]DD_branchT
|
||||
branchCount int
|
||||
coverSplit DD_rectT
|
||||
coverSplitArea float64
|
||||
}
|
||||
|
||||
func DD_New() *DD_RTree {
|
||||
// We only support machine word size simple data type eg. integer index or object pointer.
|
||||
// Since we are storing as union with non data branch
|
||||
return &DD_RTree{
|
||||
root: &DD_nodeT{},
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert entry
|
||||
/// \param a_min Min of bounding rect
|
||||
/// \param a_max Max of bounding rect
|
||||
/// \param a_dataId Positive Id of data. Maybe zero, but negative numbers not allowed.
|
||||
func (tr *DD_RTree) Insert(min, max [DD_numDims]float64, dataId interface{}) {
|
||||
var branch DD_branchT
|
||||
branch.data = dataId
|
||||
for axis := 0; axis < DD_numDims; axis++ {
|
||||
branch.rect.min[axis] = min[axis]
|
||||
branch.rect.max[axis] = max[axis]
|
||||
}
|
||||
DD_insertRect(&branch, &tr.root, 0)
|
||||
}
|
||||
|
||||
/// Remove entry
|
||||
/// \param a_min Min of bounding rect
|
||||
/// \param a_max Max of bounding rect
|
||||
/// \param a_dataId Positive Id of data. Maybe zero, but negative numbers not allowed.
|
||||
func (tr *DD_RTree) Remove(min, max [DD_numDims]float64, dataId interface{}) {
|
||||
var rect DD_rectT
|
||||
for axis := 0; axis < DD_numDims; axis++ {
|
||||
rect.min[axis] = min[axis]
|
||||
rect.max[axis] = max[axis]
|
||||
}
|
||||
DD_removeRect(&rect, dataId, &tr.root)
|
||||
}
|
||||
|
||||
/// Find all within DD_search rectangle
|
||||
/// \param a_min Min of DD_search bounding rect
|
||||
/// \param a_max Max of DD_search bounding rect
|
||||
/// \param a_searchResult DD_search result array. Caller should set grow size. Function will reset, not append to array.
|
||||
/// \param a_resultCallback Callback function to return result. Callback should return 'true' to continue searching
|
||||
/// \param a_context User context to pass as parameter to a_resultCallback
|
||||
/// \return Returns the number of entries found
|
||||
func (tr *DD_RTree) Search(min, max [DD_numDims]float64, resultCallback func(data interface{}) bool) int {
|
||||
var rect DD_rectT
|
||||
for axis := 0; axis < DD_numDims; axis++ {
|
||||
rect.min[axis] = min[axis]
|
||||
rect.max[axis] = max[axis]
|
||||
}
|
||||
foundCount, _ := DD_search(tr.root, rect, 0, resultCallback)
|
||||
return foundCount
|
||||
}
|
||||
|
||||
/// Count the data elements in this container. This is slow as no internal counter is maintained.
|
||||
func (tr *DD_RTree) Count() int {
|
||||
var count int
|
||||
DD_countRec(tr.root, &count)
|
||||
return count
|
||||
}
|
||||
|
||||
/// Remove all entries from tree
|
||||
func (tr *DD_RTree) RemoveAll() {
|
||||
// Delete all existing nodes
|
||||
tr.root = &DD_nodeT{}
|
||||
}
|
||||
|
||||
func DD_countRec(node *DD_nodeT, count *int) {
|
||||
if node.isInternalNode() { // not a leaf node
|
||||
for index := 0; index < node.count; index++ {
|
||||
DD_countRec(node.branch[index].child, count)
|
||||
}
|
||||
} else { // A leaf node
|
||||
*count += node.count
|
||||
}
|
||||
}
|
||||
|
||||
// Inserts a new data rectangle into the index structure.
|
||||
// Recursively descends tree, propagates splits back up.
|
||||
// Returns 0 if node was not split. Old node updated.
|
||||
// If node was split, returns 1 and sets the pointer pointed to by
|
||||
// new_node to point to the new node. Old node updated to become one of two.
|
||||
// The level argument specifies the number of steps up from the leaf
|
||||
// level to insert; e.g. a data rectangle goes in at level = 0.
|
||||
func DD_insertRectRec(branch *DD_branchT, node *DD_nodeT, newNode **DD_nodeT, level int) bool {
|
||||
// recurse until we reach the correct level for the new record. data records
|
||||
// will always be called with a_level == 0 (leaf)
|
||||
if node.level > level {
|
||||
// Still above level for insertion, go down tree recursively
|
||||
var otherNode *DD_nodeT
|
||||
//var newBranch DD_branchT
|
||||
|
||||
// find the optimal branch for this record
|
||||
index := DD_pickBranch(&branch.rect, node)
|
||||
|
||||
// recursively insert this record into the picked branch
|
||||
childWasSplit := DD_insertRectRec(branch, node.branch[index].child, &otherNode, level)
|
||||
|
||||
if !childWasSplit {
|
||||
// Child was not split. Merge the bounding box of the new record with the
|
||||
// existing bounding box
|
||||
node.branch[index].rect = DD_combineRect(&branch.rect, &(node.branch[index].rect))
|
||||
return false
|
||||
} else {
|
||||
// Child was split. The old branches are now re-partitioned to two nodes
|
||||
// so we have to re-calculate the bounding boxes of each node
|
||||
node.branch[index].rect = DD_nodeCover(node.branch[index].child)
|
||||
var newBranch DD_branchT
|
||||
newBranch.child = otherNode
|
||||
newBranch.rect = DD_nodeCover(otherNode)
|
||||
|
||||
// The old node is already a child of a_node. Now add the newly-created
|
||||
// node to a_node as well. a_node might be split because of that.
|
||||
return DD_addBranch(&newBranch, node, newNode)
|
||||
}
|
||||
} else if node.level == level {
|
||||
// We have reached level for insertion. Add rect, split if necessary
|
||||
return DD_addBranch(branch, node, newNode)
|
||||
} else {
|
||||
// Should never occur
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Insert a data rectangle into an index structure.
|
||||
// DD_insertRect provides for splitting the root;
|
||||
// returns 1 if root was split, 0 if it was not.
|
||||
// The level argument specifies the number of steps up from the leaf
|
||||
// level to insert; e.g. a data rectangle goes in at level = 0.
|
||||
// InsertRect2 does the recursion.
|
||||
//
|
||||
func DD_insertRect(branch *DD_branchT, root **DD_nodeT, level int) bool {
|
||||
var newNode *DD_nodeT
|
||||
|
||||
if DD_insertRectRec(branch, *root, &newNode, level) { // Root split
|
||||
|
||||
// Grow tree taller and new root
|
||||
newRoot := &DD_nodeT{}
|
||||
newRoot.level = (*root).level + 1
|
||||
|
||||
var newBranch DD_branchT
|
||||
|
||||
// add old root node as a child of the new root
|
||||
newBranch.rect = DD_nodeCover(*root)
|
||||
newBranch.child = *root
|
||||
DD_addBranch(&newBranch, newRoot, nil)
|
||||
|
||||
// add the split node as a child of the new root
|
||||
newBranch.rect = DD_nodeCover(newNode)
|
||||
newBranch.child = newNode
|
||||
DD_addBranch(&newBranch, newRoot, nil)
|
||||
|
||||
// set the new root as the root node
|
||||
*root = newRoot
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Find the smallest rectangle that includes all rectangles in branches of a node.
|
||||
func DD_nodeCover(node *DD_nodeT) DD_rectT {
|
||||
rect := node.branch[0].rect
|
||||
for index := 1; index < node.count; index++ {
|
||||
rect = DD_combineRect(&rect, &(node.branch[index].rect))
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
// Add a branch to a node. Split the node if necessary.
|
||||
// Returns 0 if node not split. Old node updated.
|
||||
// Returns 1 if node split, sets *new_node to address of new node.
|
||||
// Old node updated, becomes one of two.
|
||||
func DD_addBranch(branch *DD_branchT, node *DD_nodeT, newNode **DD_nodeT) bool {
|
||||
if node.count < DD_maxNodes { // Split won't be necessary
|
||||
node.branch[node.count] = *branch
|
||||
node.count++
|
||||
return false
|
||||
} else {
|
||||
DD_splitNode(node, branch, newNode)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Disconnect a dependent node.
|
||||
// Caller must return (or stop using iteration index) after this as count has changed
|
||||
func DD_disconnectBranch(node *DD_nodeT, index int) {
|
||||
// Remove element by swapping with the last element to prevent gaps in array
|
||||
node.branch[index] = node.branch[node.count-1]
|
||||
node.branch[node.count-1].data = nil
|
||||
node.branch[node.count-1].child = nil
|
||||
node.count--
|
||||
}
|
||||
|
||||
// Pick a branch. Pick the one that will need the smallest increase
|
||||
// in area to accomodate the new rectangle. This will result in the
|
||||
// least total area for the covering rectangles in the current node.
|
||||
// In case of a tie, pick the one which was smaller before, to get
|
||||
// the best resolution when searching.
|
||||
func DD_pickBranch(rect *DD_rectT, node *DD_nodeT) int {
|
||||
var firstTime bool = true
|
||||
var increase float64
|
||||
var bestIncr float64 = -1
|
||||
var area float64
|
||||
var bestArea float64
|
||||
var best int
|
||||
var tempRect DD_rectT
|
||||
|
||||
for index := 0; index < node.count; index++ {
|
||||
curRect := &node.branch[index].rect
|
||||
area = DD_calcRectVolume(curRect)
|
||||
tempRect = DD_combineRect(rect, curRect)
|
||||
increase = DD_calcRectVolume(&tempRect) - area
|
||||
if (increase < bestIncr) || firstTime {
|
||||
best = index
|
||||
bestArea = area
|
||||
bestIncr = increase
|
||||
firstTime = false
|
||||
} else if (increase == bestIncr) && (area < bestArea) {
|
||||
best = index
|
||||
bestArea = area
|
||||
bestIncr = increase
|
||||
}
|
||||
}
|
||||
return best
|
||||
}
|
||||
|
||||
// Combine two rectangles into larger one containing both
|
||||
func DD_combineRect(rectA, rectB *DD_rectT) DD_rectT {
|
||||
var newRect DD_rectT
|
||||
|
||||
for index := 0; index < DD_numDims; index++ {
|
||||
newRect.min[index] = DD_fmin(rectA.min[index], rectB.min[index])
|
||||
newRect.max[index] = DD_fmax(rectA.max[index], rectB.max[index])
|
||||
}
|
||||
|
||||
return newRect
|
||||
}
|
||||
|
||||
// Split a node.
|
||||
// Divides the nodes branches and the extra one between two nodes.
|
||||
// Old node is one of the new ones, and one really new one is created.
|
||||
// Tries more than one method for choosing a partition, uses best result.
|
||||
func DD_splitNode(node *DD_nodeT, branch *DD_branchT, newNode **DD_nodeT) {
|
||||
// Could just use local here, but member or external is faster since it is reused
|
||||
var localVars DD_partitionVarsT
|
||||
parVars := &localVars
|
||||
|
||||
// Load all the branches into a buffer, initialize old node
|
||||
DD_getBranches(node, branch, parVars)
|
||||
|
||||
// Find partition
|
||||
DD_choosePartition(parVars, DD_minNodes)
|
||||
|
||||
// Create a new node to hold (about) half of the branches
|
||||
*newNode = &DD_nodeT{}
|
||||
(*newNode).level = node.level
|
||||
|
||||
// Put branches from buffer into 2 nodes according to the chosen partition
|
||||
node.count = 0
|
||||
DD_loadNodes(node, *newNode, parVars)
|
||||
}
|
||||
|
||||
// Calculate the n-dimensional volume of a rectangle
|
||||
func DD_rectVolume(rect *DD_rectT) float64 {
|
||||
var volume float64 = 1
|
||||
for index := 0; index < DD_numDims; index++ {
|
||||
volume *= rect.max[index] - rect.min[index]
|
||||
}
|
||||
return volume
|
||||
}
|
||||
|
||||
// The exact volume of the bounding sphere for the given DD_rectT
|
||||
func DD_rectSphericalVolume(rect *DD_rectT) float64 {
|
||||
var sumOfSquares float64 = 0
|
||||
var radius float64
|
||||
|
||||
for index := 0; index < DD_numDims; index++ {
|
||||
halfExtent := (rect.max[index] - rect.min[index]) * 0.5
|
||||
sumOfSquares += halfExtent * halfExtent
|
||||
}
|
||||
|
||||
radius = math.Sqrt(sumOfSquares)
|
||||
|
||||
// Pow maybe slow, so test for common dims just use x*x, x*x*x.
|
||||
if DD_numDims == 5 {
|
||||
return (radius * radius * radius * radius * radius * DD_unitSphereVolume)
|
||||
} else if DD_numDims == 4 {
|
||||
return (radius * radius * radius * radius * DD_unitSphereVolume)
|
||||
} else if DD_numDims == 3 {
|
||||
return (radius * radius * radius * DD_unitSphereVolume)
|
||||
} else if DD_numDims == 2 {
|
||||
return (radius * radius * DD_unitSphereVolume)
|
||||
} else {
|
||||
return (math.Pow(radius, DD_numDims) * DD_unitSphereVolume)
|
||||
}
|
||||
}
|
||||
|
||||
// Use one of the methods to calculate retangle volume
|
||||
func DD_calcRectVolume(rect *DD_rectT) float64 {
|
||||
if DD_useSphericalVolume {
|
||||
return DD_rectSphericalVolume(rect) // Slower but helps certain merge cases
|
||||
} else { // RTREE_USE_SPHERICAL_VOLUME
|
||||
return DD_rectVolume(rect) // Faster but can cause poor merges
|
||||
} // RTREE_USE_SPHERICAL_VOLUME
|
||||
}
|
||||
|
||||
// Load branch buffer with branches from full node plus the extra branch.
|
||||
func DD_getBranches(node *DD_nodeT, branch *DD_branchT, parVars *DD_partitionVarsT) {
|
||||
// Load the branch buffer
|
||||
for index := 0; index < DD_maxNodes; index++ {
|
||||
parVars.branchBuf[index] = node.branch[index]
|
||||
}
|
||||
parVars.branchBuf[DD_maxNodes] = *branch
|
||||
parVars.branchCount = DD_maxNodes + 1
|
||||
|
||||
// Calculate rect containing all in the set
|
||||
parVars.coverSplit = parVars.branchBuf[0].rect
|
||||
for index := 1; index < DD_maxNodes+1; index++ {
|
||||
parVars.coverSplit = DD_combineRect(&parVars.coverSplit, &parVars.branchBuf[index].rect)
|
||||
}
|
||||
parVars.coverSplitArea = DD_calcRectVolume(&parVars.coverSplit)
|
||||
}
|
||||
|
||||
// Method #0 for choosing a partition:
|
||||
// As the seeds for the two groups, pick the two rects that would waste the
|
||||
// most area if covered by a single rectangle, i.e. evidently the worst pair
|
||||
// to have in the same group.
|
||||
// Of the remaining, one at a time is chosen to be put in one of the two groups.
|
||||
// The one chosen is the one with the greatest difference in area expansion
|
||||
// depending on which group - the rect most strongly attracted to one group
|
||||
// and repelled from the other.
|
||||
// If one group gets too full (more would force other group to violate min
|
||||
// fill requirement) then other group gets the rest.
|
||||
// These last are the ones that can go in either group most easily.
|
||||
func DD_choosePartition(parVars *DD_partitionVarsT, minFill int) {
|
||||
var biggestDiff float64
|
||||
var group, chosen, betterGroup int
|
||||
|
||||
DD_initParVars(parVars, parVars.branchCount, minFill)
|
||||
DD_pickSeeds(parVars)
|
||||
|
||||
for ((parVars.count[0] + parVars.count[1]) < parVars.total) &&
|
||||
(parVars.count[0] < (parVars.total - parVars.minFill)) &&
|
||||
(parVars.count[1] < (parVars.total - parVars.minFill)) {
|
||||
biggestDiff = -1
|
||||
for index := 0; index < parVars.total; index++ {
|
||||
if DD_notTaken == parVars.partition[index] {
|
||||
curRect := &parVars.branchBuf[index].rect
|
||||
rect0 := DD_combineRect(curRect, &parVars.cover[0])
|
||||
rect1 := DD_combineRect(curRect, &parVars.cover[1])
|
||||
growth0 := DD_calcRectVolume(&rect0) - parVars.area[0]
|
||||
growth1 := DD_calcRectVolume(&rect1) - parVars.area[1]
|
||||
diff := growth1 - growth0
|
||||
if diff >= 0 {
|
||||
group = 0
|
||||
} else {
|
||||
group = 1
|
||||
diff = -diff
|
||||
}
|
||||
|
||||
if diff > biggestDiff {
|
||||
biggestDiff = diff
|
||||
chosen = index
|
||||
betterGroup = group
|
||||
} else if (diff == biggestDiff) && (parVars.count[group] < parVars.count[betterGroup]) {
|
||||
chosen = index
|
||||
betterGroup = group
|
||||
}
|
||||
}
|
||||
}
|
||||
DD_classify(chosen, betterGroup, parVars)
|
||||
}
|
||||
|
||||
// If one group too full, put remaining rects in the other
|
||||
if (parVars.count[0] + parVars.count[1]) < parVars.total {
|
||||
if parVars.count[0] >= parVars.total-parVars.minFill {
|
||||
group = 1
|
||||
} else {
|
||||
group = 0
|
||||
}
|
||||
for index := 0; index < parVars.total; index++ {
|
||||
if DD_notTaken == parVars.partition[index] {
|
||||
DD_classify(index, group, parVars)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Copy branches from the buffer into two nodes according to the partition.
|
||||
func DD_loadNodes(nodeA, nodeB *DD_nodeT, parVars *DD_partitionVarsT) {
|
||||
for index := 0; index < parVars.total; index++ {
|
||||
targetNodeIndex := parVars.partition[index]
|
||||
targetNodes := []*DD_nodeT{nodeA, nodeB}
|
||||
|
||||
// It is assured that DD_addBranch here will not cause a node split.
|
||||
DD_addBranch(&parVars.branchBuf[index], targetNodes[targetNodeIndex], nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize a DD_partitionVarsT structure.
|
||||
func DD_initParVars(parVars *DD_partitionVarsT, maxRects, minFill int) {
|
||||
parVars.count[0] = 0
|
||||
parVars.count[1] = 0
|
||||
parVars.area[0] = 0
|
||||
parVars.area[1] = 0
|
||||
parVars.total = maxRects
|
||||
parVars.minFill = minFill
|
||||
for index := 0; index < maxRects; index++ {
|
||||
parVars.partition[index] = DD_notTaken
|
||||
}
|
||||
}
|
||||
|
||||
func DD_pickSeeds(parVars *DD_partitionVarsT) {
|
||||
var seed0, seed1 int
|
||||
var worst, waste float64
|
||||
var area [DD_maxNodes + 1]float64
|
||||
|
||||
for index := 0; index < parVars.total; index++ {
|
||||
area[index] = DD_calcRectVolume(&parVars.branchBuf[index].rect)
|
||||
}
|
||||
|
||||
worst = -parVars.coverSplitArea - 1
|
||||
for indexA := 0; indexA < parVars.total-1; indexA++ {
|
||||
for indexB := indexA + 1; indexB < parVars.total; indexB++ {
|
||||
oneRect := DD_combineRect(&parVars.branchBuf[indexA].rect, &parVars.branchBuf[indexB].rect)
|
||||
waste = DD_calcRectVolume(&oneRect) - area[indexA] - area[indexB]
|
||||
if waste > worst {
|
||||
worst = waste
|
||||
seed0 = indexA
|
||||
seed1 = indexB
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DD_classify(seed0, 0, parVars)
|
||||
DD_classify(seed1, 1, parVars)
|
||||
}
|
||||
|
||||
// Put a branch in one of the groups.
|
||||
func DD_classify(index, group int, parVars *DD_partitionVarsT) {
|
||||
parVars.partition[index] = group
|
||||
|
||||
// Calculate combined rect
|
||||
if parVars.count[group] == 0 {
|
||||
parVars.cover[group] = parVars.branchBuf[index].rect
|
||||
} else {
|
||||
parVars.cover[group] = DD_combineRect(&parVars.branchBuf[index].rect, &parVars.cover[group])
|
||||
}
|
||||
|
||||
// Calculate volume of combined rect
|
||||
parVars.area[group] = DD_calcRectVolume(&parVars.cover[group])
|
||||
|
||||
parVars.count[group]++
|
||||
}
|
||||
|
||||
// Delete a data rectangle from an index structure.
|
||||
// Pass in a pointer to a DD_rectT, the tid of the record, ptr to ptr to root node.
|
||||
// Returns 1 if record not found, 0 if success.
|
||||
// DD_removeRect provides for eliminating the root.
|
||||
func DD_removeRect(rect *DD_rectT, id interface{}, root **DD_nodeT) bool {
|
||||
var reInsertList *DD_listNodeT
|
||||
|
||||
if !DD_removeRectRec(rect, id, *root, &reInsertList) {
|
||||
// Found and deleted a data item
|
||||
// Reinsert any branches from eliminated nodes
|
||||
for reInsertList != nil {
|
||||
tempNode := reInsertList.node
|
||||
|
||||
for index := 0; index < tempNode.count; index++ {
|
||||
// TODO go over this code. should I use (tempNode->m_level - 1)?
|
||||
DD_insertRect(&tempNode.branch[index], root, tempNode.level)
|
||||
}
|
||||
reInsertList = reInsertList.next
|
||||
}
|
||||
|
||||
// Check for redundant root (not leaf, 1 child) and eliminate TODO replace
|
||||
// if with while? In case there is a whole branch of redundant roots...
|
||||
if (*root).count == 1 && (*root).isInternalNode() {
|
||||
tempNode := (*root).branch[0].child
|
||||
*root = tempNode
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Delete a rectangle from non-root part of an index structure.
|
||||
// Called by DD_removeRect. Descends tree recursively,
|
||||
// merges branches on the way back up.
|
||||
// Returns 1 if record not found, 0 if success.
|
||||
func DD_removeRectRec(rect *DD_rectT, id interface{}, node *DD_nodeT, listNode **DD_listNodeT) bool {
|
||||
if node.isInternalNode() { // not a leaf node
|
||||
for index := 0; index < node.count; index++ {
|
||||
if DD_overlap(*rect, node.branch[index].rect) {
|
||||
if !DD_removeRectRec(rect, id, node.branch[index].child, listNode) {
|
||||
if node.branch[index].child.count >= DD_minNodes {
|
||||
// child removed, just resize parent rect
|
||||
node.branch[index].rect = DD_nodeCover(node.branch[index].child)
|
||||
} else {
|
||||
// child removed, not enough entries in node, eliminate node
|
||||
DD_reInsert(node.branch[index].child, listNode)
|
||||
DD_disconnectBranch(node, index) // Must return after this call as count has changed
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
} else { // A leaf node
|
||||
for index := 0; index < node.count; index++ {
|
||||
if node.branch[index].data == id {
|
||||
DD_disconnectBranch(node, index) // Must return after this call as count has changed
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Decide whether two rectangles DD_overlap.
|
||||
func DD_overlap(rectA, rectB DD_rectT) bool {
|
||||
for index := 0; index < DD_numDims; index++ {
|
||||
if rectA.min[index] > rectB.max[index] ||
|
||||
rectB.min[index] > rectA.max[index] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Add a node to the reinsertion list. All its branches will later
|
||||
// be reinserted into the index structure.
|
||||
func DD_reInsert(node *DD_nodeT, listNode **DD_listNodeT) {
|
||||
newListNode := &DD_listNodeT{}
|
||||
newListNode.node = node
|
||||
newListNode.next = *listNode
|
||||
*listNode = newListNode
|
||||
}
|
||||
|
||||
// DD_search in an index tree or subtree for all data retangles that DD_overlap the argument rectangle.
|
||||
func DD_search(node *DD_nodeT, rect DD_rectT, foundCount int, resultCallback func(data interface{}) bool) (int, bool) {
|
||||
if node.isInternalNode() {
|
||||
// This is an internal node in the tree
|
||||
for index := 0; index < node.count; index++ {
|
||||
if DD_overlap(rect, node.branch[index].rect) {
|
||||
var ok bool
|
||||
foundCount, ok = DD_search(node.branch[index].child, rect, foundCount, resultCallback)
|
||||
if !ok {
|
||||
// The callback indicated to stop searching
|
||||
return foundCount, false
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// This is a leaf node
|
||||
for index := 0; index < node.count; index++ {
|
||||
if DD_overlap(rect, node.branch[index].rect) {
|
||||
id := node.branch[index].data
|
||||
foundCount++
|
||||
if !resultCallback(id) {
|
||||
return foundCount, false // Don't continue searching
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
return foundCount, true // Continue searching
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -167,3 +167,59 @@ func BenchmarkInsert(t *testing.B) {
|
|||
|
||||
t.StartTimer()
|
||||
}
|
||||
|
||||
func TestKNN(t *testing.T) {
|
||||
n := 25000
|
||||
tr := New(nil)
|
||||
var points []*tPoint
|
||||
rand.Seed(1)
|
||||
for i := 0; i < n; i++ {
|
||||
r := tRandPoint()
|
||||
points = append(points, r)
|
||||
tr.Insert(r)
|
||||
}
|
||||
if tr.Count() != n {
|
||||
t.Fatalf("expecting %v, got %v", n, tr.Count())
|
||||
}
|
||||
var count int
|
||||
tr.Search(&tRect{-100, -100, -100, -100, 100, 100, 100, 100}, func(item Item) bool {
|
||||
count++
|
||||
return true
|
||||
})
|
||||
var pdist float64
|
||||
var i int
|
||||
center := []float64{50, 50}
|
||||
centerRect := &tRect{center[0], center[1], center[0], center[1]}
|
||||
tr.KNN(centerRect, true, func(item Item, dist float64) bool {
|
||||
dist2 := boxDistPoint(center, item)
|
||||
if i > 0 && dist2 < pdist {
|
||||
t.Fatal("out of order")
|
||||
}
|
||||
pdist = dist
|
||||
i++
|
||||
return true
|
||||
})
|
||||
if i != n {
|
||||
t.Fatal("mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func boxDistPoint(point []float64, item Item) float64 {
|
||||
var dist float64
|
||||
min, max := item.Rect(nil)
|
||||
for i := 0; i < len(point); i++ {
|
||||
d := axisDist(point[i], min[i], max[i])
|
||||
dist += d * d
|
||||
}
|
||||
return dist
|
||||
}
|
||||
|
||||
func axisDist(k, min, max float64) float64 {
|
||||
if k < min {
|
||||
return min - k
|
||||
}
|
||||
if k <= max {
|
||||
return 0
|
||||
}
|
||||
return k - max
|
||||
}
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
ISC License
|
||||
|
||||
Copyright (c) 2017, Vladimir Agafonkin
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any purpose
|
||||
with or without fee is hereby granted, provided that the above copyright notice
|
||||
and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
|
||||
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
||||
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
|
||||
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
|
||||
THIS SOFTWARE.
|
|
@ -0,0 +1,7 @@
|
|||
# tinyqueue
|
||||
<a href="https://godoc.org/github.com/tidwall/tinyqueue"><img src="https://img.shields.io/badge/api-reference-blue.svg?style=flat-square" alt="GoDoc"></a>
|
||||
|
||||
tinyqueue is a Go package for binary heap priority queues.
|
||||
Ported from the [tinyqueue](https://github.com/mourner/tinyqueue) Javascript library.
|
||||
|
||||
|
|
@ -0,0 +1,86 @@
|
|||
package tinyqueue
|
||||
|
||||
type Queue struct {
|
||||
length int
|
||||
data []Item
|
||||
}
|
||||
|
||||
type Item interface {
|
||||
Less(Item) bool
|
||||
}
|
||||
|
||||
func New(data []Item) *Queue {
|
||||
q := &Queue{}
|
||||
q.data = data
|
||||
q.length = len(data)
|
||||
if q.length > 0 {
|
||||
i := q.length >> 1
|
||||
for ; i >= 0; i-- {
|
||||
q.down(i)
|
||||
}
|
||||
}
|
||||
return q
|
||||
}
|
||||
|
||||
func (q *Queue) Push(item Item) {
|
||||
q.data = append(q.data, item)
|
||||
q.length++
|
||||
q.up(q.length - 1)
|
||||
}
|
||||
func (q *Queue) Pop() Item {
|
||||
if q.length == 0 {
|
||||
return nil
|
||||
}
|
||||
top := q.data[0]
|
||||
q.length--
|
||||
if q.length > 0 {
|
||||
q.data[0] = q.data[q.length]
|
||||
q.down(0)
|
||||
}
|
||||
q.data = q.data[:len(q.data)-1]
|
||||
return top
|
||||
}
|
||||
func (q *Queue) Peek() Item {
|
||||
if q.length == 0 {
|
||||
return nil
|
||||
}
|
||||
return q.data[0]
|
||||
}
|
||||
func (q *Queue) Len() int {
|
||||
return q.length
|
||||
}
|
||||
func (q *Queue) down(pos int) {
|
||||
data := q.data
|
||||
halfLength := q.length >> 1
|
||||
item := data[pos]
|
||||
for pos < halfLength {
|
||||
left := (pos << 1) + 1
|
||||
right := left + 1
|
||||
best := data[left]
|
||||
if right < q.length && data[right].Less(best) {
|
||||
left = right
|
||||
best = data[right]
|
||||
}
|
||||
if !best.Less(item) {
|
||||
break
|
||||
}
|
||||
data[pos] = best
|
||||
pos = left
|
||||
}
|
||||
data[pos] = item
|
||||
}
|
||||
|
||||
func (q *Queue) up(pos int) {
|
||||
data := q.data
|
||||
item := data[pos]
|
||||
for pos > 0 {
|
||||
parent := (pos - 1) >> 1
|
||||
current := data[parent]
|
||||
if !item.Less(current) {
|
||||
break
|
||||
}
|
||||
data[pos] = current
|
||||
pos = parent
|
||||
}
|
||||
data[pos] = item
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package tinyqueue
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type floatValue float64
|
||||
|
||||
func (a floatValue) Less(b Item) bool {
|
||||
return a < b.(floatValue)
|
||||
}
|
||||
|
||||
var data, sorted = func() ([]Item, []Item) {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
var data []Item
|
||||
for i := 0; i < 100; i++ {
|
||||
data = append(data, floatValue(rand.Float64()*100))
|
||||
}
|
||||
sorted := make([]Item, len(data))
|
||||
copy(sorted, data)
|
||||
sort.Slice(sorted, func(i, j int) bool {
|
||||
return sorted[i].Less(sorted[j])
|
||||
})
|
||||
return data, sorted
|
||||
}()
|
||||
|
||||
func TestMaintainsPriorityQueue(t *testing.T) {
|
||||
q := New(nil)
|
||||
for i := 0; i < len(data); i++ {
|
||||
q.Push(data[i])
|
||||
}
|
||||
assert.Equal(t, q.Peek(), sorted[0])
|
||||
var result []Item
|
||||
for q.length > 0 {
|
||||
result = append(result, q.Pop())
|
||||
}
|
||||
assert.Equal(t, result, sorted)
|
||||
}
|
||||
|
||||
func TestAcceptsDataInConstructor(t *testing.T) {
|
||||
q := New(data)
|
||||
var result []Item
|
||||
for q.length > 0 {
|
||||
result = append(result, q.Pop())
|
||||
}
|
||||
assert.Equal(t, result, sorted)
|
||||
}
|
||||
func TestHandlesEdgeCasesWithFewElements(t *testing.T) {
|
||||
q := New(nil)
|
||||
q.Push(floatValue(2))
|
||||
q.Push(floatValue(1))
|
||||
q.Pop()
|
||||
q.Pop()
|
||||
q.Pop()
|
||||
q.Push(floatValue(2))
|
||||
q.Push(floatValue(1))
|
||||
assert.Equal(t, float64(q.Pop().(floatValue)), 1.0)
|
||||
assert.Equal(t, float64(q.Pop().(floatValue)), 2.0)
|
||||
assert.Equal(t, q.Pop(), nil)
|
||||
}
|
Loading…
Reference in New Issue