Faster point in polygon / GeoJSON updates
The big change is that the GeoJSON package has been completely rewritten to fix a few of geometry calculation bugs, increase performance, and to better follow the GeoJSON spec RFC 7946. GeoJSON updates - A LineString now requires at least two points. - All json members, even foreign, now persist with the object. - The bbox member persists too but is no longer used for geometry calculations. This is change in behavior. Previously Tile38 would treat the bbox as the object's physical rectangle. - Corrections to geometry intersects and within calculations. Faster spatial queries - The performance of Point-in-polygon and object intersect operations are greatly improved for complex polygons and line strings. It went from O(n) to roughly O(log n). - The same for all collection types with many children, including FeatureCollection, GeometryCollection, MultiPoint, MultiLineString, and MultiPolygon. Codebase changes - The pkg directory has been renamed to internal - The GeoJSON internal package has been moved to a seperate repo at https://github.com/tidwall/geojson. It's now vendored. Please look out for higher memory usage for datasets using complex shapes. A complex shape is one that has 64 or more points. For these shapes it's expected that there will be increase of least 54 bytes per point.
|
@ -133,6 +133,34 @@
|
||||||
pruneopts = ""
|
pruneopts = ""
|
||||||
revision = "0b12d6b5"
|
revision = "0b12d6b5"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:75dddee0eb82002b5aff6937fdf6d544b85322d2414524a521768fe4b4e5ed3d"
|
||||||
|
name = "github.com/mmcloughlin/geohash"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = ""
|
||||||
|
revision = "f7f2bcae3294530249c63fcb6fb6d5e83eee4e73"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:f04a78a43f55f089c919beee8ec4a1495dee1bd271548da2cb44bf44699a6a61"
|
||||||
|
name = "github.com/nats-io/go-nats"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"encoders/builtin",
|
||||||
|
"util",
|
||||||
|
]
|
||||||
|
pruneopts = ""
|
||||||
|
revision = "fb0396ee0bdb8018b0fef30d6d1de798ce99cd05"
|
||||||
|
version = "v1.6.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
digest = "1:be61e8224b84064109eaba8157cbb4bbe6ca12443e182b6624fdfa1c0dcf53d9"
|
||||||
|
name = "github.com/nats-io/nuid"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = ""
|
||||||
|
revision = "289cccf02c178dc782430d534e3c1f5b72af807f"
|
||||||
|
version = "v1.0.0"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:30f72985e574101b71666d6e601e7564bd02d95164da59ca17363ad194137969"
|
digest = "1:30f72985e574101b71666d6e601e7564bd02d95164da59ca17363ad194137969"
|
||||||
|
@ -197,6 +225,18 @@
|
||||||
pruneopts = ""
|
pruneopts = ""
|
||||||
revision = "b67b1b8c1658cb01502801c14e33c61e6c4cbb95"
|
revision = "b67b1b8c1658cb01502801c14e33c61e6c4cbb95"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:157eb52179752d4d88f1049aa6c3e4954d6796af5f6bcd54b5ab40f8637805df"
|
||||||
|
name = "github.com/tidwall/geojson"
|
||||||
|
packages = [
|
||||||
|
".",
|
||||||
|
"geo",
|
||||||
|
"geometry",
|
||||||
|
]
|
||||||
|
pruneopts = ""
|
||||||
|
revision = "8ff3ef500c61617c9f325603cf40863ca7086a1d"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:211773b67c5594aa92b1e8389c59558fa4927614507ea38237265e00c0ba6b81"
|
digest = "1:211773b67c5594aa92b1e8389c59558fa4927614507ea38237265e00c0ba6b81"
|
||||||
name = "github.com/tidwall/gjson"
|
name = "github.com/tidwall/gjson"
|
||||||
|
@ -229,6 +269,14 @@
|
||||||
pruneopts = ""
|
pruneopts = ""
|
||||||
revision = "1731857f09b1f38450e2c12409748407822dc6be"
|
revision = "1731857f09b1f38450e2c12409748407822dc6be"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
digest = "1:7eed51dcae60e95dbde54662594ef90a7cbf3b7e3f0de32f84f0213b695967ff"
|
||||||
|
name = "github.com/tidwall/pretty"
|
||||||
|
packages = ["."]
|
||||||
|
pruneopts = ""
|
||||||
|
revision = "65a9db5fad5105a89e17f38adcc9878685be6d78"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:630381558bc538e831db8468dd0dc2702d81789f79b8ddf665eeebc729e2a055"
|
digest = "1:630381558bc538e831db8468dd0dc2702d81789f79b8ddf665eeebc729e2a055"
|
||||||
|
@ -394,13 +442,18 @@
|
||||||
"github.com/eclipse/paho.mqtt.golang",
|
"github.com/eclipse/paho.mqtt.golang",
|
||||||
"github.com/garyburd/redigo/redis",
|
"github.com/garyburd/redigo/redis",
|
||||||
"github.com/golang/protobuf/proto",
|
"github.com/golang/protobuf/proto",
|
||||||
|
"github.com/mmcloughlin/geohash",
|
||||||
|
"github.com/nats-io/go-nats",
|
||||||
"github.com/peterh/liner",
|
"github.com/peterh/liner",
|
||||||
"github.com/streadway/amqp",
|
"github.com/streadway/amqp",
|
||||||
"github.com/tidwall/boxtree/d2",
|
"github.com/tidwall/boxtree/d2",
|
||||||
"github.com/tidwall/btree",
|
"github.com/tidwall/btree",
|
||||||
"github.com/tidwall/buntdb",
|
"github.com/tidwall/buntdb",
|
||||||
|
"github.com/tidwall/geojson",
|
||||||
|
"github.com/tidwall/geojson/geometry",
|
||||||
"github.com/tidwall/gjson",
|
"github.com/tidwall/gjson",
|
||||||
"github.com/tidwall/lotsa",
|
"github.com/tidwall/lotsa",
|
||||||
|
"github.com/tidwall/match",
|
||||||
"github.com/tidwall/redbench",
|
"github.com/tidwall/redbench",
|
||||||
"github.com/tidwall/redcon",
|
"github.com/tidwall/redcon",
|
||||||
"github.com/tidwall/resp",
|
"github.com/tidwall/resp",
|
||||||
|
|
14
Gopkg.toml
|
@ -20,7 +20,11 @@
|
||||||
# name = "github.com/x/y"
|
# name = "github.com/x/y"
|
||||||
# version = "2.4.0"
|
# version = "2.4.0"
|
||||||
|
|
||||||
required = ["github.com/tidwall/lotsa"]
|
required = [
|
||||||
|
"github.com/tidwall/lotsa",
|
||||||
|
"github.com/mmcloughlin/geohash",
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
]
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
|
@ -50,10 +54,6 @@ required = ["github.com/tidwall/lotsa"]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/streadway/amqp"
|
name = "github.com/streadway/amqp"
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
name = "github.com/stretchr/testify"
|
|
||||||
version = "1.1.4"
|
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "github.com/tidwall/btree"
|
name = "github.com/tidwall/btree"
|
||||||
|
@ -82,10 +82,6 @@ required = ["github.com/tidwall/lotsa"]
|
||||||
name = "github.com/tidwall/sjson"
|
name = "github.com/tidwall/sjson"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/tidwall/tinyqueue"
|
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
name = "golang.org/x/crypto"
|
name = "golang.org/x/crypto"
|
||||||
|
|
20
README.md
|
@ -1,6 +1,6 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="http://tile38.com"><img
|
<a href="http://tile38.com"><img
|
||||||
src="/pkg/assets/logo1500.png"
|
src="/internal/assets/logo1500.png"
|
||||||
width="200" height="200" border="0" alt="Tile38"></a>
|
width="200" height="200" border="0" alt="Tile38"></a>
|
||||||
</p>
|
</p>
|
||||||
<p align="center">
|
<p align="center">
|
||||||
|
@ -14,11 +14,11 @@ Tile38 is an open source (MIT licensed), in-memory geolocation data store, spati
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<i>This README is quick start document. You can find detailed documentation at <a href="http://tile38.com">http://tile38.com</a>.</i><br><br>
|
<i>This README is quick start document. You can find detailed documentation at <a href="http://tile38.com">http://tile38.com</a>.</i><br><br>
|
||||||
<a href="#searching"><img src="/pkg/assets/search-nearby.png" alt="Nearby" border="0" width="120" height="120"></a>
|
<a href="#searching"><img src="/internal/assets/search-nearby.png" alt="Nearby" border="0" width="120" height="120"></a>
|
||||||
<a href="#searching"><img src="/pkg/assets/search-within.png" alt="Within" border="0" width="120" height="120"></a>
|
<a href="#searching"><img src="/internal/assets/search-within.png" alt="Within" border="0" width="120" height="120"></a>
|
||||||
<a href="#searching"><img src="/pkg/assets/search-intersects.png" alt="Intersects" border="0" width="120" height="120"></a>
|
<a href="#searching"><img src="/internal/assets/search-intersects.png" alt="Intersects" border="0" width="120" height="120"></a>
|
||||||
<a href="http://tile38.com/topics/geofencing"><img src="/pkg/assets/geofence.gif" alt="Geofencing" border="0" width="120" height="120"></a>
|
<a href="http://tile38.com/topics/geofencing"><img src="/internal/assets/geofence.gif" alt="Geofencing" border="0" width="120" height="120"></a>
|
||||||
<a href="http://tile38.com/topics/roaming-geofences"><img src="/pkg/assets/roaming.gif" alt="Roaming Geofences" border="0" width="120" height="120"></a>
|
<a href="http://tile38.com/topics/roaming-geofences"><img src="/internal/assets/roaming.gif" alt="Roaming Geofences" border="0" width="120" height="120"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
@ -134,19 +134,19 @@ To set a field when an object already exists:
|
||||||
|
|
||||||
Tile38 has support to search for objects and points that are within or intersects other objects. All object types can be searched including Polygons, MultiPolygons, GeometryCollections, etc.
|
Tile38 has support to search for objects and points that are within or intersects other objects. All object types can be searched including Polygons, MultiPolygons, GeometryCollections, etc.
|
||||||
|
|
||||||
<img src="/pkg/assets/search-within.png" width="200" height="200" border="0" alt="Search Within" align="left">
|
<img src="/internal/assets/search-within.png" width="200" height="200" border="0" alt="Search Within" align="left">
|
||||||
|
|
||||||
#### Within
|
#### Within
|
||||||
WITHIN searches a collection for objects that are fully contained inside a specified bounding area.
|
WITHIN searches a collection for objects that are fully contained inside a specified bounding area.
|
||||||
<BR CLEAR="ALL">
|
<BR CLEAR="ALL">
|
||||||
|
|
||||||
<img src="/pkg/assets/search-intersects.png" width="200" height="200" border="0" alt="Search Intersects" align="left">
|
<img src="/internal/assets/search-intersects.png" width="200" height="200" border="0" alt="Search Intersects" align="left">
|
||||||
|
|
||||||
#### Intersects
|
#### Intersects
|
||||||
INTERSECTS searches a collection for objects that intersect a specified bounding area.
|
INTERSECTS searches a collection for objects that intersect a specified bounding area.
|
||||||
<BR CLEAR="ALL">
|
<BR CLEAR="ALL">
|
||||||
|
|
||||||
<img src="/pkg/assets/search-nearby.png" width="200" height="200" border="0" alt="Search Nearby" align="left">
|
<img src="/internal/assets/search-nearby.png" width="200" height="200" border="0" alt="Search Nearby" align="left">
|
||||||
|
|
||||||
#### Nearby
|
#### Nearby
|
||||||
NEARBY searches a collection for objects that intersect a specified radius.
|
NEARBY searches a collection for objects that intersect a specified radius.
|
||||||
|
@ -167,7 +167,7 @@ NEARBY searches a collection for objects that intersect a specified radius.
|
||||||
|
|
||||||
## Geofencing
|
## Geofencing
|
||||||
|
|
||||||
<img src="/pkg/assets/geofence.gif" width="200" height="200" border="0" alt="Geofence animation" align="left">
|
<img src="/internal/assets/geofence.gif" width="200" height="200" border="0" alt="Geofence animation" align="left">
|
||||||
A <a href="https://en.wikipedia.org/wiki/Geo-fence">geofence</a> is a virtual boundary that can detect when an object enters or exits the area. This boundary can be a radius, bounding box, or a polygon. Tile38 can turn any standard search into a geofence monitor by adding the FENCE keyword to the search.
|
A <a href="https://en.wikipedia.org/wiki/Geo-fence">geofence</a> is a virtual boundary that can detect when an object enters or exits the area. This boundary can be a radius, bounding box, or a polygon. Tile38 can turn any standard search into a geofence monitor by adding the FENCE keyword to the search.
|
||||||
|
|
||||||
*Tile38 also allows for [Webhooks](http://tile38.com/commands/sethook) to be assigned to Geofences.*
|
*Tile38 also allows for [Webhooks](http://tile38.com/commands/sethook) to be assigned to Geofences.*
|
||||||
|
|
4
build.sh
|
@ -161,7 +161,7 @@ if [ "$NOLINK" != "1" ]; then
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# generate the core package
|
# generate the core package
|
||||||
pkg/core/gen.sh
|
core/gen.sh
|
||||||
|
|
||||||
# build and store objects into original directory.
|
# build and store objects into original directory.
|
||||||
CGO_ENABLED=0 go build -ldflags "$LDFLAGS -extldflags '-static'" -o "$OD/tile38-server" cmd/tile38-server/*.go
|
CGO_ENABLED=0 go build -ldflags "$LDFLAGS -extldflags '-static'" -o "$OD/tile38-server" cmd/tile38-server/*.go
|
||||||
|
@ -178,7 +178,7 @@ if [ "$1" == "test" ]; then
|
||||||
}
|
}
|
||||||
trap testend EXIT
|
trap testend EXIT
|
||||||
cd tests && go test && cd ..
|
cd tests && go test && cd ..
|
||||||
go test $(go list ./... | grep -v /vendor/ | grep -v /tests)
|
go test $(go list ./... | grep -v /vendor/ | grep -v /tests | grep -v /pkg/geojson_bak)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# cover if requested
|
# cover if requested
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/redbench"
|
"github.com/tidwall/redbench"
|
||||||
"github.com/tidwall/tile38/pkg/core"
|
"github.com/tidwall/tile38/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
|
@ -16,8 +16,8 @@ import (
|
||||||
"github.com/peterh/liner"
|
"github.com/peterh/liner"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/client"
|
"github.com/tidwall/tile38/core"
|
||||||
"github.com/tidwall/tile38/pkg/core"
|
"github.com/tidwall/tile38/internal/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
func userHomeDir() string {
|
func userHomeDir() string {
|
||||||
|
|
|
@ -15,14 +15,12 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/tidwall/tile38/core"
|
||||||
|
"github.com/tidwall/tile38/internal/controller"
|
||||||
|
"github.com/tidwall/tile38/internal/hservice"
|
||||||
|
"github.com/tidwall/tile38/internal/log"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
|
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
|
||||||
"github.com/tidwall/tile38/pkg/controller"
|
|
||||||
"github.com/tidwall/tile38/pkg/core"
|
|
||||||
"github.com/tidwall/tile38/pkg/hservice"
|
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
Before Width: | Height: | Size: 139 KiB After Width: | Height: | Size: 139 KiB |
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 985 KiB After Width: | Height: | Size: 985 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 97 KiB |
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 110 KiB |
Before Width: | Height: | Size: 252 KiB After Width: | Height: | Size: 252 KiB |
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 269 KiB |
Before Width: | Height: | Size: 196 KiB After Width: | Height: | Size: 196 KiB |
|
@ -0,0 +1,142 @@
|
||||||
|
package clip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clip clips the contents of a geojson object and return
|
||||||
|
func Clip(obj geojson.Object, clipper geojson.Object) (clipped geojson.Object) {
|
||||||
|
switch obj := obj.(type) {
|
||||||
|
case *geojson.Point:
|
||||||
|
return clipPoint(obj, clipper)
|
||||||
|
case *geojson.Rect:
|
||||||
|
return clipRect(obj, clipper)
|
||||||
|
case *geojson.LineString:
|
||||||
|
return clipLineString(obj, clipper)
|
||||||
|
case *geojson.Polygon:
|
||||||
|
return clipPolygon(obj, clipper)
|
||||||
|
case *geojson.Feature:
|
||||||
|
return clipFeature(obj, clipper)
|
||||||
|
case geojson.Collection:
|
||||||
|
return clipCollection(obj, clipper)
|
||||||
|
}
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
// clipSegment is Cohen-Sutherland Line Clipping
|
||||||
|
// https://www.cs.helsinki.fi/group/goa/viewing/leikkaus/lineClip.html
|
||||||
|
func clipSegment(seg geometry.Segment, rect geometry.Rect) (
|
||||||
|
res geometry.Segment, rejected bool,
|
||||||
|
) {
|
||||||
|
startCode := getCode(rect, seg.A)
|
||||||
|
endCode := getCode(rect, seg.B)
|
||||||
|
if (startCode | endCode) == 0 {
|
||||||
|
// trivially accept
|
||||||
|
res = seg
|
||||||
|
} else if (startCode & endCode) != 0 {
|
||||||
|
// trivially reject
|
||||||
|
rejected = true
|
||||||
|
} else if startCode != 0 {
|
||||||
|
// start is outside. get new start.
|
||||||
|
newStart := intersect(rect, startCode, seg.A, seg.B)
|
||||||
|
res, rejected =
|
||||||
|
clipSegment(geometry.Segment{A: newStart, B: seg.B}, rect)
|
||||||
|
} else {
|
||||||
|
// end is outside. get new end.
|
||||||
|
newEnd := intersect(rect, endCode, seg.A, seg.B)
|
||||||
|
res, rejected = clipSegment(geometry.Segment{A: seg.A, B: newEnd}, rect)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// clipRing is Sutherland-Hodgman Polygon Clipping
|
||||||
|
// https://www.cs.helsinki.fi/group/goa/viewing/leikkaus/intro2.html
|
||||||
|
func clipRing(ring []geometry.Point, bbox geometry.Rect) (
|
||||||
|
resRing []geometry.Point,
|
||||||
|
) {
|
||||||
|
if len(ring) < 4 {
|
||||||
|
// under 4 elements this is not a polygon ring!
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var edge uint8
|
||||||
|
var inside, prevInside bool
|
||||||
|
var prev geometry.Point
|
||||||
|
for edge = 1; edge <= 8; edge *= 2 {
|
||||||
|
prev = ring[len(ring)-2]
|
||||||
|
prevInside = (getCode(bbox, prev) & edge) == 0
|
||||||
|
for _, p := range ring {
|
||||||
|
inside = (getCode(bbox, p) & edge) == 0
|
||||||
|
if prevInside && inside {
|
||||||
|
// Staying inside
|
||||||
|
resRing = append(resRing, p)
|
||||||
|
} else if prevInside && !inside {
|
||||||
|
// Leaving
|
||||||
|
resRing = append(resRing, intersect(bbox, edge, prev, p))
|
||||||
|
} else if !prevInside && inside {
|
||||||
|
// Entering
|
||||||
|
resRing = append(resRing, intersect(bbox, edge, prev, p))
|
||||||
|
resRing = append(resRing, p)
|
||||||
|
} else {
|
||||||
|
// Staying outside
|
||||||
|
}
|
||||||
|
prev, prevInside = p, inside
|
||||||
|
}
|
||||||
|
if len(resRing) > 0 && resRing[0] != resRing[len(resRing)-1] {
|
||||||
|
resRing = append(resRing, resRing[0])
|
||||||
|
}
|
||||||
|
ring, resRing = resRing, []geometry.Point{}
|
||||||
|
if len(ring) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resRing = ring
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCode(bbox geometry.Rect, point geometry.Point) (code uint8) {
|
||||||
|
code = 0
|
||||||
|
|
||||||
|
if point.X < bbox.Min.X {
|
||||||
|
code |= 1 // left
|
||||||
|
} else if point.X > bbox.Max.X {
|
||||||
|
code |= 2 // right
|
||||||
|
}
|
||||||
|
|
||||||
|
if point.Y < bbox.Min.Y {
|
||||||
|
code |= 4 // bottom
|
||||||
|
} else if point.Y > bbox.Max.Y {
|
||||||
|
code |= 8 // top
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func intersect(bbox geometry.Rect, code uint8, start, end geometry.Point) (
|
||||||
|
new geometry.Point,
|
||||||
|
) {
|
||||||
|
if (code & 8) != 0 { // top
|
||||||
|
new = geometry.Point{
|
||||||
|
X: start.X + (end.X-start.X)*(bbox.Max.Y-start.Y)/(end.Y-start.Y),
|
||||||
|
Y: bbox.Max.Y,
|
||||||
|
}
|
||||||
|
} else if (code & 4) != 0 { // bottom
|
||||||
|
new = geometry.Point{
|
||||||
|
X: start.X + (end.X-start.X)*(bbox.Min.Y-start.Y)/(end.Y-start.Y),
|
||||||
|
Y: bbox.Min.Y,
|
||||||
|
}
|
||||||
|
} else if (code & 2) != 0 { //right
|
||||||
|
new = geometry.Point{
|
||||||
|
X: bbox.Max.X,
|
||||||
|
Y: start.Y + (end.Y-start.Y)*(bbox.Max.X-start.X)/(end.X-start.X),
|
||||||
|
}
|
||||||
|
} else if (code & 1) != 0 { // left
|
||||||
|
new = geometry.Point{
|
||||||
|
X: bbox.Min.X,
|
||||||
|
Y: start.Y + (end.Y-start.Y)*(bbox.Min.X-start.X)/(end.X-start.X),
|
||||||
|
}
|
||||||
|
} else { // should not call intersect with the zero code
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
package clip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func LO(points []geometry.Point) *geojson.LineString {
|
||||||
|
return geojson.NewLineString(geometry.NewLine(points, geometry.DefaultIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
func RO(minX, minY, maxX, maxY float64) *geojson.Rect {
|
||||||
|
return geojson.NewRect(geometry.Rect{
|
||||||
|
Min: geometry.Point{X: 1.5, Y: 0.5},
|
||||||
|
Max: geometry.Point{X: 2.5, Y: 1.8},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func PPO(exterior []geometry.Point, holes [][]geometry.Point) *geojson.Polygon {
|
||||||
|
return geojson.NewPolygon(geometry.NewPoly(exterior, holes, geometry.DefaultIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClipLineStringSimple(t *testing.T) {
|
||||||
|
ls := LO([]geometry.Point{
|
||||||
|
{X: 1, Y: 1},
|
||||||
|
{X: 2, Y: 2},
|
||||||
|
{X: 3, Y: 1}})
|
||||||
|
clipped := Clip(ls, RO(1.5, 0.5, 2.5, 1.8))
|
||||||
|
cl, ok := clipped.(*geojson.MultiLineString)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("wrong type")
|
||||||
|
}
|
||||||
|
if len(cl.Children()) != 2 {
|
||||||
|
t.Fatal("result must have two parts in MultiString")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClipPolygonSimple(t *testing.T) {
|
||||||
|
exterior := []geometry.Point{
|
||||||
|
{X: 2, Y: 2},
|
||||||
|
{X: 1, Y: 2},
|
||||||
|
{X: 1.5, Y: 1.5},
|
||||||
|
{X: 1, Y: 1},
|
||||||
|
{X: 2, Y: 1},
|
||||||
|
{X: 2, Y: 2},
|
||||||
|
}
|
||||||
|
holes := [][]geometry.Point{
|
||||||
|
[]geometry.Point{
|
||||||
|
{X: 1.9, Y: 1.9},
|
||||||
|
{X: 1.2, Y: 1.9},
|
||||||
|
{X: 1.45, Y: 1.65},
|
||||||
|
{X: 1.9, Y: 1.5},
|
||||||
|
{X: 1.9, Y: 1.9},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
polygon := PPO(exterior, holes)
|
||||||
|
clipped := Clip(polygon, RO(1.3, 1.3, 1.4, 2.15))
|
||||||
|
cp, ok := clipped.(*geojson.Polygon)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("wrong type")
|
||||||
|
}
|
||||||
|
if !cp.Base().Exterior.Empty() && len(cp.Base().Holes) != 1 {
|
||||||
|
t.Fatal("result must have two parts in Polygon")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// func TestClipLineString(t *testing.T) {
|
||||||
|
// featuresJSON := `
|
||||||
|
// {"type": "FeatureCollection","features": [
|
||||||
|
// {"type": "Feature","properties":{},"geometry": {"type": "LineString","coordinates": [[-71.46537780761717,42.594290856363344],[-71.37714385986328,42.600861802789524],[-71.37508392333984,42.538156868495555],[-71.43756866455078,42.535374141307415],[-71.44683837890625,42.466018925787495],[-71.334228515625,42.465005871175755],[-71.32736206054688,42.52424199254517]]}},
|
||||||
|
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.49284362792969,42.527784255084676],[-71.35791778564453,42.527784255084676],[-71.35791778564453,42.61096959812047],[-71.49284362792969,42.61096959812047],[-71.49284362792969,42.527784255084676]]]}},
|
||||||
|
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.47396087646484,42.48247876554176],[-71.30744934082031,42.48247876554176],[-71.30744934082031,42.576596402826894],[-71.47396087646484,42.576596402826894],[-71.47396087646484,42.48247876554176]]]}},
|
||||||
|
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.33491516113281,42.613496290695196],[-71.29920959472656,42.613496290695196],[-71.29920959472656,42.643556064374536],[-71.33491516113281,42.643556064374536],[-71.33491516113281,42.613496290695196]]]}},
|
||||||
|
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.37130737304686,42.530061317794775],[-71.3287353515625,42.530061317794775],[-71.3287353515625,42.60414701616359],[-71.37130737304686,42.60414701616359],[-71.37130737304686,42.530061317794775]]]}},
|
||||||
|
// {"type": "Feature","properties":{},"geometry": {"type": "Polygon","coordinates": [[[-71.52889251708984,42.564460160624115],[-71.45713806152342,42.54043355305221],[-71.53266906738281,42.49969365675931],[-71.36547088623047,42.508552415528634],[-71.43962860107422,42.58999409368092],[-71.52889251708984,42.564460160624115]]]}},
|
||||||
|
// {"type": "Feature","properties": {},"geometry": {"type": "Point","coordinates": [-71.33079528808594,42.55940269610327]}},
|
||||||
|
// {"type": "Feature","properties": {},"geometry": {"type": "Point","coordinates": [-71.27208709716797,42.53107331902133]}}
|
||||||
|
// ]}
|
||||||
|
// `
|
||||||
|
// rectJSON := `{"type": "Feature","properties": {},"geometry": {"type": "Polygon","coordinates": [[[-71.44065856933594,42.51740991900762],[-71.29131317138672,42.51740991900762],[-71.29131317138672,42.62663343969058],[-71.44065856933594,42.62663343969058],[-71.44065856933594,42.51740991900762]]]}}`
|
||||||
|
// features := expectJSON(t, featuresJSON, nil)
|
||||||
|
// rect := expectJSON(t, rectJSON, nil)
|
||||||
|
// clipped := features.Clipped(rect)
|
||||||
|
// println(clipped.String())
|
||||||
|
|
||||||
|
// }
|
|
@ -0,0 +1,20 @@
|
||||||
|
package clip
|
||||||
|
|
||||||
|
import "github.com/tidwall/geojson"
|
||||||
|
|
||||||
|
func clipCollection(
|
||||||
|
collection geojson.Collection, clipper geojson.Object,
|
||||||
|
) geojson.Object {
|
||||||
|
var features []geojson.Object
|
||||||
|
for _, feature := range collection.Children() {
|
||||||
|
feature = Clip(feature, clipper)
|
||||||
|
if feature.Empty() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := feature.(*geojson.Feature); !ok {
|
||||||
|
feature = geojson.NewFeature(feature, "")
|
||||||
|
}
|
||||||
|
features = append(features, feature)
|
||||||
|
}
|
||||||
|
return geojson.NewFeatureCollection(features)
|
||||||
|
}
|
|
@ -0,0 +1,13 @@
|
||||||
|
package clip
|
||||||
|
|
||||||
|
import "github.com/tidwall/geojson"
|
||||||
|
|
||||||
|
func clipFeature(
|
||||||
|
feature *geojson.Feature, clipper geojson.Object,
|
||||||
|
) geojson.Object {
|
||||||
|
newFeature := Clip(feature.Base(), clipper)
|
||||||
|
if _, ok := newFeature.(*geojson.Feature); !ok {
|
||||||
|
newFeature = geojson.NewFeature(newFeature, feature.Members())
|
||||||
|
}
|
||||||
|
return newFeature
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package clip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func clipLineString(
|
||||||
|
lineString *geojson.LineString, clipper geojson.Object,
|
||||||
|
) geojson.Object {
|
||||||
|
bbox := clipper.Rect()
|
||||||
|
var newPoints [][]geometry.Point
|
||||||
|
var clipped geometry.Segment
|
||||||
|
var rejected bool
|
||||||
|
var line []geometry.Point
|
||||||
|
base := lineString.Base()
|
||||||
|
nSegments := base.NumSegments()
|
||||||
|
for i := 0; i < nSegments; i++ {
|
||||||
|
clipped, rejected = clipSegment(base.SegmentAt(i), bbox)
|
||||||
|
if rejected {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(line) > 0 && line[len(line)-1] != clipped.A {
|
||||||
|
newPoints = append(newPoints, line)
|
||||||
|
line = []geometry.Point{clipped.A}
|
||||||
|
} else if len(line) == 0 {
|
||||||
|
line = append(line, clipped.A)
|
||||||
|
}
|
||||||
|
line = append(line, clipped.B)
|
||||||
|
}
|
||||||
|
if len(line) > 0 {
|
||||||
|
newPoints = append(newPoints, line)
|
||||||
|
}
|
||||||
|
var children []*geometry.Line
|
||||||
|
for _, points := range newPoints {
|
||||||
|
children = append(children,
|
||||||
|
geometry.NewLine(points, geometry.DefaultIndex))
|
||||||
|
}
|
||||||
|
if len(children) == 1 {
|
||||||
|
return geojson.NewLineString(children[0])
|
||||||
|
}
|
||||||
|
return geojson.NewMultiLineString(children)
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package clip
|
||||||
|
|
||||||
|
import "github.com/tidwall/geojson"
|
||||||
|
|
||||||
|
func clipPoint(point *geojson.Point, clipper geojson.Object) geojson.Object {
|
||||||
|
if point.IntersectsRect(clipper.Rect()) {
|
||||||
|
return point
|
||||||
|
}
|
||||||
|
return geojson.NewMultiPoint(nil)
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package clip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func clipPolygon(
|
||||||
|
polygon *geojson.Polygon, clipper geojson.Object,
|
||||||
|
) geojson.Object {
|
||||||
|
rect := clipper.Rect()
|
||||||
|
var newPoints [][]geometry.Point
|
||||||
|
base := polygon.Base()
|
||||||
|
rings := []geometry.Ring{base.Exterior}
|
||||||
|
rings = append(rings, base.Holes...)
|
||||||
|
for _, ring := range rings {
|
||||||
|
ringPoints := make([]geometry.Point, ring.NumPoints())
|
||||||
|
for i := 0; i < len(ringPoints); i++ {
|
||||||
|
ringPoints[i] = ring.PointAt(i)
|
||||||
|
}
|
||||||
|
newPoints = append(newPoints, clipRing(ringPoints, rect))
|
||||||
|
}
|
||||||
|
|
||||||
|
var exterior []geometry.Point
|
||||||
|
var holes [][]geometry.Point
|
||||||
|
if len(newPoints) > 0 {
|
||||||
|
exterior = newPoints[0]
|
||||||
|
}
|
||||||
|
if len(newPoints) > 1 {
|
||||||
|
holes = newPoints[1:]
|
||||||
|
}
|
||||||
|
newPoly := geojson.NewPolygon(
|
||||||
|
geometry.NewPoly(exterior, holes, geometry.DefaultIndex),
|
||||||
|
)
|
||||||
|
if newPoly.Empty() {
|
||||||
|
return geojson.NewMultiPolygon(nil)
|
||||||
|
}
|
||||||
|
return polygon
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
package clip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func clipRect(rect *geojson.Rect, clipper geojson.Object) geojson.Object {
|
||||||
|
base := rect.Base()
|
||||||
|
points := make([]geometry.Point, base.NumPoints())
|
||||||
|
for i := 0; i < len(points); i++ {
|
||||||
|
points[i] = base.PointAt(i)
|
||||||
|
}
|
||||||
|
poly := geometry.NewPoly(points, nil, geometry.DefaultIndex)
|
||||||
|
gPoly := geojson.NewPolygon(poly)
|
||||||
|
return Clip(gPoly, clipper)
|
||||||
|
}
|
|
@ -0,0 +1,570 @@
|
||||||
|
package collection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/tidwall/boxtree/d2"
|
||||||
|
"github.com/tidwall/btree"
|
||||||
|
"github.com/tidwall/tile38/internal/ds"
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
|
)
|
||||||
|
|
||||||
|
type itemT struct {
|
||||||
|
id string
|
||||||
|
obj geojson.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
func (item *itemT) Less(other btree.Item, ctx interface{}) bool {
|
||||||
|
value1 := item.obj.String()
|
||||||
|
value2 := other.(*itemT).obj.String()
|
||||||
|
if value1 < value2 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if value1 > value2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// the values match so we'll compare IDs, which are always unique.
|
||||||
|
return item.id < other.(*itemT).id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collection represents a collection of geojson objects.
|
||||||
|
type Collection struct {
|
||||||
|
items ds.BTree // items sorted by keys
|
||||||
|
index d2.BoxTree // items geospatially indexed
|
||||||
|
values *btree.BTree // items sorted by value+key
|
||||||
|
fieldMap map[string]int
|
||||||
|
fieldValues map[string][]float64
|
||||||
|
weight int
|
||||||
|
points int
|
||||||
|
objects int // geometry count
|
||||||
|
nobjects int // non-geometry count
|
||||||
|
}
|
||||||
|
|
||||||
|
var counter uint64
|
||||||
|
|
||||||
|
// New creates an empty collection
|
||||||
|
func New() *Collection {
|
||||||
|
col := &Collection{
|
||||||
|
values: btree.New(16, nil),
|
||||||
|
fieldMap: make(map[string]int),
|
||||||
|
}
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collection) setFieldValues(id string, values []float64) {
|
||||||
|
if c.fieldValues == nil {
|
||||||
|
c.fieldValues = make(map[string][]float64)
|
||||||
|
}
|
||||||
|
c.fieldValues[id] = values
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collection) getFieldValues(id string) (values []float64) {
|
||||||
|
if c.fieldValues == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return c.fieldValues[id]
|
||||||
|
}
|
||||||
|
func (c *Collection) deleteFieldValues(id string) {
|
||||||
|
if c.fieldValues != nil {
|
||||||
|
delete(c.fieldValues, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count returns the number of objects in collection.
|
||||||
|
func (c *Collection) Count() int {
|
||||||
|
return c.objects + c.nobjects
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringCount returns the number of string values.
|
||||||
|
func (c *Collection) StringCount() int {
|
||||||
|
return c.nobjects
|
||||||
|
}
|
||||||
|
|
||||||
|
// PointCount returns the number of points (lat/lon coordinates) in collection.
|
||||||
|
func (c *Collection) PointCount() int {
|
||||||
|
return c.points
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalWeight calculates the in-memory cost of the collection in bytes.
|
||||||
|
func (c *Collection) TotalWeight() int {
|
||||||
|
return c.weight
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds returns the bounds of all the items in the collection.
|
||||||
|
func (c *Collection) Bounds() (minX, minY, maxX, maxY float64) {
|
||||||
|
min, max := c.index.Bounds()
|
||||||
|
if len(min) >= 2 && len(max) >= 2 {
|
||||||
|
return min[0], min[1], max[0], max[1]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func objIsSpatial(obj geojson.Object) bool {
|
||||||
|
_, ok := obj.(geojson.Spatial)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collection) objWeight(item *itemT) int {
|
||||||
|
var weight int
|
||||||
|
if objIsSpatial(item.obj) {
|
||||||
|
weight = item.obj.NumPoints() * 16
|
||||||
|
} else {
|
||||||
|
weight = len(item.obj.String())
|
||||||
|
}
|
||||||
|
return weight + len(c.getFieldValues(item.id))*8 + len(item.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collection) indexDelete(item *itemT) {
|
||||||
|
if !item.obj.Empty() {
|
||||||
|
rect := item.obj.Rect()
|
||||||
|
c.index.Delete(
|
||||||
|
[]float64{rect.Min.X, rect.Min.Y},
|
||||||
|
[]float64{rect.Max.X, rect.Max.Y},
|
||||||
|
item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collection) indexInsert(item *itemT) {
|
||||||
|
if !item.obj.Empty() {
|
||||||
|
rect := item.obj.Rect()
|
||||||
|
c.index.Insert(
|
||||||
|
[]float64{rect.Min.X, rect.Min.Y},
|
||||||
|
[]float64{rect.Max.X, rect.Max.Y},
|
||||||
|
item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set adds or replaces an object in the collection and returns the fields
|
||||||
|
// array. If an item with the same id is already in the collection then the
|
||||||
|
// new item will adopt the old item's fields.
|
||||||
|
// The fields argument is optional.
|
||||||
|
// The return values are the old object, the old fields, and the new fields
|
||||||
|
func (c *Collection) Set(
|
||||||
|
id string, obj geojson.Object, fields []string, values []float64,
|
||||||
|
) (
|
||||||
|
oldObject geojson.Object, oldFields []float64, newFields []float64,
|
||||||
|
) {
|
||||||
|
newItem := &itemT{id: id, obj: obj}
|
||||||
|
|
||||||
|
// add the new item to main btree and remove the old one if needed
|
||||||
|
oldItem, ok := c.items.Set(id, newItem)
|
||||||
|
if ok {
|
||||||
|
oldItem := oldItem.(*itemT)
|
||||||
|
// the old item was removed, now let's remove it from the rtree/btree.
|
||||||
|
if objIsSpatial(oldItem.obj) {
|
||||||
|
c.indexDelete(oldItem)
|
||||||
|
c.objects--
|
||||||
|
} else {
|
||||||
|
c.values.Delete(oldItem)
|
||||||
|
c.nobjects--
|
||||||
|
}
|
||||||
|
|
||||||
|
// decrement the point count
|
||||||
|
c.points -= oldItem.obj.NumPoints()
|
||||||
|
|
||||||
|
// decrement the weights
|
||||||
|
c.weight -= c.objWeight(oldItem)
|
||||||
|
|
||||||
|
// references
|
||||||
|
oldObject = oldItem.obj
|
||||||
|
oldFields = c.getFieldValues(id)
|
||||||
|
newFields = oldFields
|
||||||
|
}
|
||||||
|
// insert the new item into the rtree or strings tree.
|
||||||
|
if objIsSpatial(newItem.obj) {
|
||||||
|
c.indexInsert(newItem)
|
||||||
|
c.objects++
|
||||||
|
} else {
|
||||||
|
c.values.ReplaceOrInsert(newItem)
|
||||||
|
c.nobjects++
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment the point count
|
||||||
|
c.points += newItem.obj.NumPoints()
|
||||||
|
|
||||||
|
// add the new weights
|
||||||
|
c.weight += c.objWeight(newItem)
|
||||||
|
|
||||||
|
if fields == nil {
|
||||||
|
if len(values) > 0 {
|
||||||
|
// directly set the field values, update weight
|
||||||
|
c.weight -= len(newFields) * 8
|
||||||
|
newFields = values
|
||||||
|
c.setFieldValues(id, newFields)
|
||||||
|
c.weight += len(newFields) * 8
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// map field name to value
|
||||||
|
for i, field := range fields {
|
||||||
|
c.setField(newItem, field, values[i])
|
||||||
|
}
|
||||||
|
newFields = c.getFieldValues(id)
|
||||||
|
}
|
||||||
|
return oldObject, oldFields, newFields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes an object and returns it.
|
||||||
|
// If the object does not exist then the 'ok' return value will be false.
|
||||||
|
func (c *Collection) Delete(id string) (
|
||||||
|
obj geojson.Object, fields []float64, ok bool,
|
||||||
|
) {
|
||||||
|
oldItemV, ok := c.items.Delete(id)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
oldItem := oldItemV.(*itemT)
|
||||||
|
if objIsSpatial(oldItem.obj) {
|
||||||
|
if !oldItem.obj.Empty() {
|
||||||
|
c.indexDelete(oldItem)
|
||||||
|
}
|
||||||
|
c.objects--
|
||||||
|
} else {
|
||||||
|
c.values.Delete(oldItem)
|
||||||
|
c.nobjects--
|
||||||
|
}
|
||||||
|
c.weight -= c.objWeight(oldItem)
|
||||||
|
c.points -= oldItem.obj.NumPoints()
|
||||||
|
|
||||||
|
fields = c.getFieldValues(id)
|
||||||
|
c.deleteFieldValues(id)
|
||||||
|
return oldItem.obj, fields, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns an object.
|
||||||
|
// If the object does not exist then the 'ok' return value will be false.
|
||||||
|
func (c *Collection) Get(id string) (
|
||||||
|
obj geojson.Object, fields []float64, ok bool,
|
||||||
|
) {
|
||||||
|
itemV, ok := c.items.Get(id)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
item := itemV.(*itemT)
|
||||||
|
return item.obj, c.getFieldValues(id), true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetField set a field value for an object and returns that object.
|
||||||
|
// If the object does not exist then the 'ok' return value will be false.
|
||||||
|
func (c *Collection) SetField(id, field string, value float64) (
|
||||||
|
obj geojson.Object, fields []float64, updated bool, ok bool,
|
||||||
|
) {
|
||||||
|
itemV, ok := c.items.Get(id)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, false, false
|
||||||
|
}
|
||||||
|
item := itemV.(*itemT)
|
||||||
|
updated = c.setField(item, field, value)
|
||||||
|
return item.obj, c.getFieldValues(id), updated, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFields is similar to SetField, just setting multiple fields at once
|
||||||
|
func (c *Collection) SetFields(
|
||||||
|
id string, inFields []string, inValues []float64,
|
||||||
|
) (obj geojson.Object, fields []float64, updatedCount int, ok bool) {
|
||||||
|
itemV, ok := c.items.Get(id)
|
||||||
|
if !ok {
|
||||||
|
return nil, nil, 0, false
|
||||||
|
}
|
||||||
|
item := itemV.(*itemT)
|
||||||
|
for idx, field := range inFields {
|
||||||
|
if c.setField(item, field, inValues[idx]) {
|
||||||
|
updatedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item.obj, c.getFieldValues(id), updatedCount, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collection) setField(item *itemT, field string, value float64) (
|
||||||
|
updated bool,
|
||||||
|
) {
|
||||||
|
idx, ok := c.fieldMap[field]
|
||||||
|
if !ok {
|
||||||
|
idx = len(c.fieldMap)
|
||||||
|
c.fieldMap[field] = idx
|
||||||
|
}
|
||||||
|
fields := c.getFieldValues(item.id)
|
||||||
|
c.weight -= len(fields) * 8
|
||||||
|
for idx >= len(fields) {
|
||||||
|
fields = append(fields, 0)
|
||||||
|
}
|
||||||
|
c.weight += len(fields) * 8
|
||||||
|
ovalue := fields[idx]
|
||||||
|
fields[idx] = value
|
||||||
|
c.setFieldValues(item.id, fields)
|
||||||
|
return ovalue != value
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldMap return a maps of the field names.
|
||||||
|
func (c *Collection) FieldMap() map[string]int {
|
||||||
|
return c.fieldMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// FieldArr return an array representation of the field names.
|
||||||
|
func (c *Collection) FieldArr() []string {
|
||||||
|
arr := make([]string, len(c.fieldMap))
|
||||||
|
for field, i := range c.fieldMap {
|
||||||
|
arr[i] = field
|
||||||
|
}
|
||||||
|
return arr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan iterates though the collection ids.
|
||||||
|
func (c *Collection) Scan(desc bool,
|
||||||
|
iterator func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
|
) bool {
|
||||||
|
var keepon = true
|
||||||
|
iter := func(key string, value interface{}) bool {
|
||||||
|
iitm := value.(*itemT)
|
||||||
|
keepon = iterator(iitm.id, iitm.obj, c.getFieldValues(iitm.id))
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
if desc {
|
||||||
|
c.items.Reverse(iter)
|
||||||
|
} else {
|
||||||
|
c.items.Scan(iter)
|
||||||
|
}
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanRange iterates though the collection starting with specified id.
|
||||||
|
func (c *Collection) ScanRange(start, end string, desc bool,
|
||||||
|
iterator func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
|
) bool {
|
||||||
|
var keepon = true
|
||||||
|
iter := func(key string, value interface{}) bool {
|
||||||
|
if !desc {
|
||||||
|
if key >= end {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if key <= end {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
iitm := value.(*itemT)
|
||||||
|
keepon = iterator(iitm.id, iitm.obj, c.getFieldValues(iitm.id))
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
|
||||||
|
if desc {
|
||||||
|
c.items.Descend(start, iter)
|
||||||
|
} else {
|
||||||
|
c.items.Ascend(start, iter)
|
||||||
|
}
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchValues iterates though the collection values.
|
||||||
|
func (c *Collection) SearchValues(desc bool,
|
||||||
|
iterator func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
|
) bool {
|
||||||
|
var keepon = true
|
||||||
|
iter := func(item btree.Item) bool {
|
||||||
|
iitm := item.(*itemT)
|
||||||
|
keepon = iterator(iitm.id, iitm.obj, c.getFieldValues(iitm.id))
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
if desc {
|
||||||
|
c.values.Descend(iter)
|
||||||
|
} else {
|
||||||
|
c.values.Ascend(iter)
|
||||||
|
}
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchValuesRange iterates though the collection values.
|
||||||
|
func (c *Collection) SearchValuesRange(start, end string, desc bool,
|
||||||
|
iterator func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
|
) bool {
|
||||||
|
var keepon = true
|
||||||
|
iter := func(item btree.Item) bool {
|
||||||
|
iitm := item.(*itemT)
|
||||||
|
keepon = iterator(iitm.id, iitm.obj, c.getFieldValues(iitm.id))
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
if desc {
|
||||||
|
c.values.DescendRange(&itemT{obj: String(start)},
|
||||||
|
&itemT{obj: String(end)}, iter)
|
||||||
|
} else {
|
||||||
|
c.values.AscendRange(&itemT{obj: String(start)},
|
||||||
|
&itemT{obj: String(end)}, iter)
|
||||||
|
}
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScanGreaterOrEqual iterates though the collection starting with specified id.
|
||||||
|
func (c *Collection) ScanGreaterOrEqual(id string, desc bool,
|
||||||
|
iterator func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
|
) bool {
|
||||||
|
var keepon = true
|
||||||
|
iter := func(key string, value interface{}) bool {
|
||||||
|
iitm := value.(*itemT)
|
||||||
|
keepon = iterator(iitm.id, iitm.obj, c.getFieldValues(iitm.id))
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
if desc {
|
||||||
|
c.items.Descend(id, iter)
|
||||||
|
} else {
|
||||||
|
c.items.Ascend(id, iter)
|
||||||
|
}
|
||||||
|
return keepon
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collection) geoSearch(
|
||||||
|
rect geometry.Rect,
|
||||||
|
iter func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
|
) bool {
|
||||||
|
alive := true
|
||||||
|
c.index.Search(
|
||||||
|
[]float64{rect.Min.X, rect.Min.Y},
|
||||||
|
[]float64{rect.Max.X, rect.Max.Y},
|
||||||
|
func(_, _ []float64, itemv interface{}) bool {
|
||||||
|
item := itemv.(*itemT)
|
||||||
|
alive = iter(item.id, item.obj, c.getFieldValues(item.id))
|
||||||
|
return alive
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return alive
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Collection) geoSparse(
|
||||||
|
obj geojson.Object, sparse uint8,
|
||||||
|
iter func(id string, obj geojson.Object, fields []float64) (match, ok bool),
|
||||||
|
) bool {
|
||||||
|
matches := make(map[string]bool)
|
||||||
|
alive := true
|
||||||
|
c.geoSparseInner(obj.Rect(), sparse,
|
||||||
|
func(id string, o geojson.Object, fields []float64) (
|
||||||
|
match, ok bool,
|
||||||
|
) {
|
||||||
|
ok = true
|
||||||
|
if !matches[id] {
|
||||||
|
match, ok = iter(id, o, fields)
|
||||||
|
if match {
|
||||||
|
matches[id] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return match, ok
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return alive
|
||||||
|
}
|
||||||
|
func (c *Collection) geoSparseInner(
|
||||||
|
rect geometry.Rect, sparse uint8,
|
||||||
|
iter func(id string, obj geojson.Object, fields []float64) (match, ok bool),
|
||||||
|
) bool {
|
||||||
|
if sparse > 0 {
|
||||||
|
w := rect.Max.X - rect.Min.X
|
||||||
|
h := rect.Max.Y - rect.Min.Y
|
||||||
|
quads := [4]geometry.Rect{
|
||||||
|
geometry.Rect{
|
||||||
|
Min: geometry.Point{X: rect.Min.X, Y: rect.Min.Y + h/2},
|
||||||
|
Max: geometry.Point{X: rect.Min.X + w/2, Y: rect.Max.Y},
|
||||||
|
},
|
||||||
|
geometry.Rect{
|
||||||
|
Min: geometry.Point{X: rect.Min.X + w/2, Y: rect.Min.Y + h/2},
|
||||||
|
Max: geometry.Point{X: rect.Max.X, Y: rect.Max.Y},
|
||||||
|
},
|
||||||
|
geometry.Rect{
|
||||||
|
Min: geometry.Point{X: rect.Min.X, Y: rect.Min.Y},
|
||||||
|
Max: geometry.Point{X: rect.Min.X + w/2, Y: rect.Min.Y + h/2},
|
||||||
|
},
|
||||||
|
geometry.Rect{
|
||||||
|
Min: geometry.Point{X: rect.Min.X + w/2, Y: rect.Min.Y},
|
||||||
|
Max: geometry.Point{X: rect.Max.X, Y: rect.Min.Y + h/2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, quad := range quads {
|
||||||
|
if !c.geoSparseInner(quad, sparse-1, iter) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
alive := true
|
||||||
|
c.geoSearch(rect,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
match, ok := iter(id, obj, fields)
|
||||||
|
if !ok {
|
||||||
|
alive = false
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !match
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return alive
|
||||||
|
}
|
||||||
|
|
||||||
|
// Within returns all object that are fully contained within an object or
|
||||||
|
// bounding box. Set obj to nil in order to use the bounding box.
|
||||||
|
func (c *Collection) Within(
|
||||||
|
obj geojson.Object, sparse uint8,
|
||||||
|
iter func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
|
) bool {
|
||||||
|
if sparse > 0 {
|
||||||
|
return c.geoSparse(obj, sparse,
|
||||||
|
func(id string, o geojson.Object, fields []float64) (
|
||||||
|
match, ok bool,
|
||||||
|
) {
|
||||||
|
if match = o.Within(obj); match {
|
||||||
|
ok = iter(id, o, fields)
|
||||||
|
}
|
||||||
|
return match, ok
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return c.geoSearch(obj.Rect(),
|
||||||
|
func(id string, o geojson.Object, fields []float64) bool {
|
||||||
|
if o.Within(obj) {
|
||||||
|
return iter(id, o, fields)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersects returns all object that are intersect an object or bounding box.
|
||||||
|
// Set obj to nil in order to use the bounding box.
|
||||||
|
func (c *Collection) Intersects(
|
||||||
|
obj geojson.Object, sparse uint8,
|
||||||
|
iter func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
|
) bool {
|
||||||
|
if sparse > 0 {
|
||||||
|
return c.geoSparse(obj, sparse,
|
||||||
|
func(id string, o geojson.Object, fields []float64) (
|
||||||
|
match, ok bool,
|
||||||
|
) {
|
||||||
|
if match = o.Intersects(obj); match {
|
||||||
|
ok = iter(id, o, fields)
|
||||||
|
}
|
||||||
|
return match, ok
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return c.geoSearch(obj.Rect(),
|
||||||
|
func(id string, o geojson.Object, fields []float64) bool {
|
||||||
|
if o.Intersects(obj) {
|
||||||
|
return iter(id, o, fields)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nearby returns the nearest neighbors
|
||||||
|
func (c *Collection) Nearby(
|
||||||
|
target geojson.Object,
|
||||||
|
iter func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
|
) bool {
|
||||||
|
alive := true
|
||||||
|
center := target.Center()
|
||||||
|
c.index.Nearby(
|
||||||
|
[]float64{center.X, center.Y},
|
||||||
|
[]float64{center.X, center.Y},
|
||||||
|
func(_, _ []float64, itemv interface{}) bool {
|
||||||
|
item := itemv.(*itemT)
|
||||||
|
alive = iter(item.id, item.obj, c.getFieldValues(item.id))
|
||||||
|
return alive
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return alive
|
||||||
|
}
|
|
@ -0,0 +1,721 @@
|
||||||
|
package collection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PO(x, y float64) *geojson.Point {
|
||||||
|
return geojson.NewPoint(geometry.Point{X: x, Y: y})
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
seed := time.Now().UnixNano()
|
||||||
|
println(seed)
|
||||||
|
rand.Seed(seed)
|
||||||
|
}
|
||||||
|
|
||||||
|
func expect(t testing.TB, expect bool) {
|
||||||
|
t.Helper()
|
||||||
|
if !expect {
|
||||||
|
t.Fatal("not what you expected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func bounds(c *Collection) geometry.Rect {
|
||||||
|
minX, minY, maxX, maxY := c.Bounds()
|
||||||
|
return geometry.Rect{
|
||||||
|
Min: geometry.Point{X: minX, Y: minY},
|
||||||
|
Max: geometry.Point{X: maxX, Y: maxY},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionNewCollection(t *testing.T) {
|
||||||
|
const numItems = 10000
|
||||||
|
objs := make(map[string]geojson.Object)
|
||||||
|
c := New()
|
||||||
|
for i := 0; i < numItems; i++ {
|
||||||
|
id := strconv.FormatInt(int64(i), 10)
|
||||||
|
var obj geojson.Object
|
||||||
|
obj = PO(rand.Float64()*360-180, rand.Float64()*180-90)
|
||||||
|
objs[id] = obj
|
||||||
|
c.Set(id, obj, nil, nil)
|
||||||
|
}
|
||||||
|
count := 0
|
||||||
|
bbox := geometry.Rect{
|
||||||
|
Min: geometry.Point{X: -180, Y: -90},
|
||||||
|
Max: geometry.Point{X: 180, Y: 90},
|
||||||
|
}
|
||||||
|
c.geoSearch(bbox, func(id string, obj geojson.Object, field []float64) bool {
|
||||||
|
count++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if count != len(objs) {
|
||||||
|
t.Fatalf("count = %d, expect %d", count, len(objs))
|
||||||
|
}
|
||||||
|
count = c.Count()
|
||||||
|
if count != len(objs) {
|
||||||
|
t.Fatalf("c.Count() = %d, expect %d", count, len(objs))
|
||||||
|
}
|
||||||
|
testCollectionVerifyContents(t, c, objs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionSet(t *testing.T) {
|
||||||
|
t.Run("AddString", func(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
str1 := String("hello")
|
||||||
|
oldObject, oldFields, newFields := c.Set("str", str1, nil, nil)
|
||||||
|
expect(t, oldObject == nil)
|
||||||
|
expect(t, len(oldFields) == 0)
|
||||||
|
expect(t, len(newFields) == 0)
|
||||||
|
})
|
||||||
|
t.Run("UpdateString", func(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
str1 := String("hello")
|
||||||
|
str2 := String("world")
|
||||||
|
oldObject, oldFields, newFields := c.Set("str", str1, nil, nil)
|
||||||
|
expect(t, oldObject == nil)
|
||||||
|
expect(t, len(oldFields) == 0)
|
||||||
|
expect(t, len(newFields) == 0)
|
||||||
|
oldObject, oldFields, newFields = c.Set("str", str2, nil, nil)
|
||||||
|
expect(t, oldObject == str1)
|
||||||
|
expect(t, len(oldFields) == 0)
|
||||||
|
expect(t, len(newFields) == 0)
|
||||||
|
})
|
||||||
|
t.Run("AddPoint", func(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
point1 := PO(-112.1, 33.1)
|
||||||
|
oldObject, oldFields, newFields := c.Set("point", point1, nil, nil)
|
||||||
|
expect(t, oldObject == nil)
|
||||||
|
expect(t, len(oldFields) == 0)
|
||||||
|
expect(t, len(newFields) == 0)
|
||||||
|
})
|
||||||
|
t.Run("UpdatePoint", func(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
point1 := PO(-112.1, 33.1)
|
||||||
|
point2 := PO(-112.2, 33.2)
|
||||||
|
oldObject, oldFields, newFields := c.Set("point", point1, nil, nil)
|
||||||
|
expect(t, oldObject == nil)
|
||||||
|
expect(t, len(oldFields) == 0)
|
||||||
|
expect(t, len(newFields) == 0)
|
||||||
|
oldObject, oldFields, newFields = c.Set("point", point2, nil, nil)
|
||||||
|
expect(t, oldObject == point1)
|
||||||
|
expect(t, len(oldFields) == 0)
|
||||||
|
expect(t, len(newFields) == 0)
|
||||||
|
})
|
||||||
|
t.Run("Fields", func(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
str1 := String("hello")
|
||||||
|
fNames := []string{"a", "b", "c"}
|
||||||
|
fValues := []float64{1, 2, 3}
|
||||||
|
oldObj, oldFlds, newFlds := c.Set("str", str1, fNames, fValues)
|
||||||
|
expect(t, oldObj == nil)
|
||||||
|
expect(t, len(oldFlds) == 0)
|
||||||
|
expect(t, reflect.DeepEqual(newFlds, fValues))
|
||||||
|
str2 := String("hello")
|
||||||
|
fNames = []string{"d", "e", "f"}
|
||||||
|
fValues = []float64{4, 5, 6}
|
||||||
|
oldObj, oldFlds, newFlds = c.Set("str", str2, fNames, fValues)
|
||||||
|
expect(t, oldObj == str1)
|
||||||
|
expect(t, reflect.DeepEqual(oldFlds, []float64{1, 2, 3}))
|
||||||
|
expect(t, reflect.DeepEqual(newFlds, []float64{1, 2, 3, 4, 5, 6}))
|
||||||
|
fValues = []float64{7, 8, 9, 10, 11, 12}
|
||||||
|
oldObj, oldFlds, newFlds = c.Set("str", str1, nil, fValues)
|
||||||
|
expect(t, oldObj == str2)
|
||||||
|
expect(t, reflect.DeepEqual(oldFlds, []float64{1, 2, 3, 4, 5, 6}))
|
||||||
|
expect(t, reflect.DeepEqual(newFlds, []float64{7, 8, 9, 10, 11, 12}))
|
||||||
|
})
|
||||||
|
t.Run("Delete", func(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
|
||||||
|
c.Set("1", String("1"), nil, nil)
|
||||||
|
c.Set("2", String("2"), nil, nil)
|
||||||
|
c.Set("3", PO(1, 2), nil, nil)
|
||||||
|
|
||||||
|
expect(t, c.Count() == 3)
|
||||||
|
expect(t, c.StringCount() == 2)
|
||||||
|
expect(t, c.PointCount() == 1)
|
||||||
|
expect(t, bounds(c) == geometry.Rect{
|
||||||
|
Min: geometry.Point{X: 1, Y: 2},
|
||||||
|
Max: geometry.Point{X: 1, Y: 2}})
|
||||||
|
var v geojson.Object
|
||||||
|
var ok bool
|
||||||
|
var flds []float64
|
||||||
|
var updated bool
|
||||||
|
var updateCount int
|
||||||
|
|
||||||
|
v, _, ok = c.Delete("2")
|
||||||
|
expect(t, v.String() == "2")
|
||||||
|
expect(t, ok)
|
||||||
|
expect(t, c.Count() == 2)
|
||||||
|
expect(t, c.StringCount() == 1)
|
||||||
|
expect(t, c.PointCount() == 1)
|
||||||
|
|
||||||
|
v, _, ok = c.Delete("1")
|
||||||
|
expect(t, v.String() == "1")
|
||||||
|
expect(t, ok)
|
||||||
|
expect(t, c.Count() == 1)
|
||||||
|
expect(t, c.StringCount() == 0)
|
||||||
|
expect(t, c.PointCount() == 1)
|
||||||
|
|
||||||
|
expect(t, len(c.FieldMap()) == 0)
|
||||||
|
|
||||||
|
v, flds, updated, ok = c.SetField("3", "hello", 123)
|
||||||
|
expect(t, ok)
|
||||||
|
expect(t, reflect.DeepEqual(flds, []float64{123}))
|
||||||
|
expect(t, updated)
|
||||||
|
expect(t, c.FieldMap()["hello"] == 0)
|
||||||
|
|
||||||
|
v, flds, updated, ok = c.SetField("3", "hello", 1234)
|
||||||
|
expect(t, ok)
|
||||||
|
expect(t, reflect.DeepEqual(flds, []float64{1234}))
|
||||||
|
expect(t, updated)
|
||||||
|
|
||||||
|
v, flds, updated, ok = c.SetField("3", "hello", 1234)
|
||||||
|
expect(t, ok)
|
||||||
|
expect(t, reflect.DeepEqual(flds, []float64{1234}))
|
||||||
|
expect(t, !updated)
|
||||||
|
|
||||||
|
v, flds, updateCount, ok = c.SetFields("3",
|
||||||
|
[]string{"planet", "world"}, []float64{55, 66})
|
||||||
|
expect(t, ok)
|
||||||
|
expect(t, reflect.DeepEqual(flds, []float64{1234, 55, 66}))
|
||||||
|
expect(t, updateCount == 2)
|
||||||
|
expect(t, c.FieldMap()["hello"] == 0)
|
||||||
|
expect(t, c.FieldMap()["planet"] == 1)
|
||||||
|
expect(t, c.FieldMap()["world"] == 2)
|
||||||
|
|
||||||
|
v, _, ok = c.Delete("3")
|
||||||
|
expect(t, v.String() == `{"type":"Point","coordinates":[1,2]}`)
|
||||||
|
expect(t, ok)
|
||||||
|
expect(t, c.Count() == 0)
|
||||||
|
expect(t, c.StringCount() == 0)
|
||||||
|
expect(t, c.PointCount() == 0)
|
||||||
|
v, _, ok = c.Delete("3")
|
||||||
|
expect(t, v == nil)
|
||||||
|
expect(t, !ok)
|
||||||
|
expect(t, c.Count() == 0)
|
||||||
|
expect(t, bounds(c) == geometry.Rect{})
|
||||||
|
v, _, ok = c.Get("3")
|
||||||
|
expect(t, v == nil)
|
||||||
|
expect(t, !ok)
|
||||||
|
_, _, _, ok = c.SetField("3", "hello", 123)
|
||||||
|
expect(t, !ok)
|
||||||
|
_, _, _, ok = c.SetFields("3", []string{"hello"}, []float64{123})
|
||||||
|
expect(t, !ok)
|
||||||
|
expect(t, c.TotalWeight() == 0)
|
||||||
|
expect(t, c.FieldMap()["hello"] == 0)
|
||||||
|
expect(t, c.FieldMap()["planet"] == 1)
|
||||||
|
expect(t, c.FieldMap()["world"] == 2)
|
||||||
|
expect(t, reflect.DeepEqual(
|
||||||
|
c.FieldArr(), []string{"hello", "planet", "world"}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionScan(t *testing.T) {
|
||||||
|
N := 256
|
||||||
|
c := New()
|
||||||
|
for _, i := range rand.Perm(N) {
|
||||||
|
id := fmt.Sprintf("%04d", i)
|
||||||
|
c.Set(id, String(id), []string{"ex"}, []float64{float64(i)})
|
||||||
|
}
|
||||||
|
var n int
|
||||||
|
var prevID string
|
||||||
|
c.Scan(false, func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, id > prevID)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[0])))
|
||||||
|
n++
|
||||||
|
prevID = id
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == c.Count())
|
||||||
|
n = 0
|
||||||
|
c.Scan(true, func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, id < prevID)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[0])))
|
||||||
|
n++
|
||||||
|
prevID = id
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == c.Count())
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.ScanRange("0060", "0070", false,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, id > prevID)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[0])))
|
||||||
|
n++
|
||||||
|
prevID = id
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == 10)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.ScanRange("0070", "0060", true,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, id < prevID)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[0])))
|
||||||
|
n++
|
||||||
|
prevID = id
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == 10)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.ScanGreaterOrEqual("0070", true,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, id < prevID)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[0])))
|
||||||
|
n++
|
||||||
|
prevID = id
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == 71)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.ScanGreaterOrEqual("0070", false,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, id > prevID)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[0])))
|
||||||
|
n++
|
||||||
|
prevID = id
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == c.Count()-70)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionSearch(t *testing.T) {
|
||||||
|
N := 256
|
||||||
|
c := New()
|
||||||
|
for i, j := range rand.Perm(N) {
|
||||||
|
id := fmt.Sprintf("%04d", j)
|
||||||
|
ex := fmt.Sprintf("%04d", i)
|
||||||
|
c.Set(id, String(ex), []string{"i", "j"},
|
||||||
|
[]float64{float64(i), float64(j)})
|
||||||
|
}
|
||||||
|
var n int
|
||||||
|
var prevValue string
|
||||||
|
c.SearchValues(false, func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, obj.String() > prevValue)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[1])))
|
||||||
|
n++
|
||||||
|
prevValue = obj.String()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == c.Count())
|
||||||
|
n = 0
|
||||||
|
c.SearchValues(true, func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, obj.String() < prevValue)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[1])))
|
||||||
|
n++
|
||||||
|
prevValue = obj.String()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == c.Count())
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.SearchValuesRange("0060", "0070", false,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, obj.String() > prevValue)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[1])))
|
||||||
|
n++
|
||||||
|
prevValue = obj.String()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == 10)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.SearchValuesRange("0070", "0060", true,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
if n > 0 {
|
||||||
|
expect(t, obj.String() < prevValue)
|
||||||
|
}
|
||||||
|
expect(t, id == fmt.Sprintf("%04d", int(fields[1])))
|
||||||
|
n++
|
||||||
|
prevValue = obj.String()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
expect(t, n == 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionWeight(t *testing.T) {
|
||||||
|
c := New()
|
||||||
|
c.Set("1", String("1"), nil, nil)
|
||||||
|
expect(t, c.TotalWeight() > 0)
|
||||||
|
c.Delete("1")
|
||||||
|
expect(t, c.TotalWeight() == 0)
|
||||||
|
c.Set("1", String("1"),
|
||||||
|
[]string{"a", "b", "c"},
|
||||||
|
[]float64{1, 2, 3},
|
||||||
|
)
|
||||||
|
expect(t, c.TotalWeight() > 0)
|
||||||
|
c.Delete("1")
|
||||||
|
expect(t, c.TotalWeight() == 0)
|
||||||
|
c.Set("1", String("1"),
|
||||||
|
[]string{"a", "b", "c"},
|
||||||
|
[]float64{1, 2, 3},
|
||||||
|
)
|
||||||
|
c.Set("2", String("2"),
|
||||||
|
[]string{"d", "e", "f"},
|
||||||
|
[]float64{4, 5, 6},
|
||||||
|
)
|
||||||
|
c.Set("1", String("1"),
|
||||||
|
[]string{"d", "e", "f"},
|
||||||
|
[]float64{4, 5, 6},
|
||||||
|
)
|
||||||
|
c.Delete("1")
|
||||||
|
c.Delete("2")
|
||||||
|
expect(t, c.TotalWeight() == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSpatialSearch(t *testing.T) {
|
||||||
|
json := `
|
||||||
|
{"type":"FeatureCollection","features":[
|
||||||
|
{"type":"Feature","id":"p1","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Point","coordinates":[-71.4743041992187,42.51867517417283]}},
|
||||||
|
{"type":"Feature","id":"p2","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Point","coordinates":[-71.4056396484375,42.50197174319114]}},
|
||||||
|
{"type":"Feature","id":"p3","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Point","coordinates":[-71.4619445800781,42.49437779897246]}},
|
||||||
|
{"type":"Feature","id":"p4","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Point","coordinates":[-71.4337921142578,42.53891577257117]}},
|
||||||
|
{"type":"Feature","id":"r1","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Polygon","coordinates":[[[-71.4279556274414,42.48804880765346],[-71.37439727783203,42.48804880765346],[-71.37439727783203,42.52322988064187],[-71.4279556274414,42.52322988064187],[-71.4279556274414,42.48804880765346]]]}},
|
||||||
|
{"type":"Feature","id":"r2","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Polygon","coordinates":[[[-71.4825439453125,42.53588010092859],[-71.45027160644531,42.53588010092859],[-71.45027160644531,42.55839115400447],[-71.4825439453125,42.55839115400447],[-71.4825439453125,42.53588010092859]]]}},
|
||||||
|
{"type":"Feature","id":"r3","properties":{"marker-color":"#962d28","stroke":"#962d28","fill":"#962d28"},"geometry":{"type":"Polygon","coordinates": [[[-71.4111328125,42.53512115995963],[-71.3833236694336,42.53512115995963],[-71.3833236694336,42.54953946116446],[-71.4111328125,42.54953946116446],[-71.4111328125,42.53512115995963]]]}},
|
||||||
|
{"type":"Feature","id":"q1","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-71.55258178710938,42.51361399979923],[-71.42074584960938,42.51361399979923],[-71.42074584960938,42.59100512331456],[-71.55258178710938,42.59100512331456],[-71.55258178710938,42.51361399979923]]]}},
|
||||||
|
{"type":"Feature","id":"q2","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-71.52992248535156,42.48121277771616],[-71.36375427246092,42.48121277771616],[-71.36375427246092,42.57786045892046],[-71.52992248535156,42.57786045892046],[-71.52992248535156,42.48121277771616]]]}},
|
||||||
|
{"type":"Feature","id":"q3","properties":{},"geometry":{"type":"Polygon","coordinates":[[[-71.49490356445312,42.56673588590953],[-71.52236938476562,42.47462922809497],[-71.42898559570312,42.464499337722344],[-71.43241882324219,42.522217752342236],[-71.37954711914061,42.56420729713456],[-71.49490356445312,42.56673588590953]]]}},
|
||||||
|
{"type":"Feature","id":"q4","properties":{},"geometry":{"type":"Point","coordinates": [-71.46366119384766,42.54043355305221]}}
|
||||||
|
]}
|
||||||
|
`
|
||||||
|
p1, _ := geojson.Parse(gjson.Get(json, `features.#[id=="p1"]`).Raw, nil)
|
||||||
|
p2, _ := geojson.Parse(gjson.Get(json, `features.#[id=="p2"]`).Raw, nil)
|
||||||
|
p3, _ := geojson.Parse(gjson.Get(json, `features.#[id=="p3"]`).Raw, nil)
|
||||||
|
p4, _ := geojson.Parse(gjson.Get(json, `features.#[id=="p4"]`).Raw, nil)
|
||||||
|
r1, _ := geojson.Parse(gjson.Get(json, `features.#[id=="r1"]`).Raw, nil)
|
||||||
|
r2, _ := geojson.Parse(gjson.Get(json, `features.#[id=="r2"]`).Raw, nil)
|
||||||
|
r3, _ := geojson.Parse(gjson.Get(json, `features.#[id=="r3"]`).Raw, nil)
|
||||||
|
q1, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q1"]`).Raw, nil)
|
||||||
|
q2, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q2"]`).Raw, nil)
|
||||||
|
q3, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q3"]`).Raw, nil)
|
||||||
|
q4, _ := geojson.Parse(gjson.Get(json, `features.#[id=="q4"]`).Raw, nil)
|
||||||
|
|
||||||
|
c := New()
|
||||||
|
c.Set("p1", p1, nil, nil)
|
||||||
|
c.Set("p2", p2, nil, nil)
|
||||||
|
c.Set("p3", p3, nil, nil)
|
||||||
|
c.Set("p4", p4, nil, nil)
|
||||||
|
c.Set("r1", r1, nil, nil)
|
||||||
|
c.Set("r2", r2, nil, nil)
|
||||||
|
c.Set("r3", r3, nil, nil)
|
||||||
|
|
||||||
|
var n int
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Within(q1, 0,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 3)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Within(q2, 0,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 7)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Within(q3, 0,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 4)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Intersects(q1, 0,
|
||||||
|
func(_ string, _ geojson.Object, _ []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 4)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Intersects(q2, 0,
|
||||||
|
func(_ string, _ geojson.Object, _ []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 7)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Intersects(q3, 0,
|
||||||
|
func(_ string, _ geojson.Object, _ []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 5)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Intersects(q3, 0,
|
||||||
|
func(_ string, _ geojson.Object, _ []float64) bool {
|
||||||
|
n++
|
||||||
|
return n <= 1
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 2)
|
||||||
|
|
||||||
|
var items []geojson.Object
|
||||||
|
exitems := []geojson.Object{
|
||||||
|
r2, p1, p4, r1, p3, r3, p2,
|
||||||
|
}
|
||||||
|
c.Nearby(q4,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
items = append(items, obj)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, len(items) == 7)
|
||||||
|
expect(t, reflect.DeepEqual(items, exitems))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCollectionSparse(t *testing.T) {
|
||||||
|
rect := geojson.NewRect(geometry.Rect{
|
||||||
|
Min: geometry.Point{X: -71.598930, Y: 42.4586739},
|
||||||
|
Max: geometry.Point{X: -71.37302, Y: 42.607937},
|
||||||
|
})
|
||||||
|
N := 10000
|
||||||
|
c := New()
|
||||||
|
r := rect.Rect()
|
||||||
|
for i := 0; i < N; i++ {
|
||||||
|
x := (r.Max.X-r.Min.X)*rand.Float64() + r.Min.X
|
||||||
|
y := (r.Max.Y-r.Min.Y)*rand.Float64() + r.Min.Y
|
||||||
|
point := PO(x, y)
|
||||||
|
c.Set(fmt.Sprintf("%d", i), point, nil, nil)
|
||||||
|
}
|
||||||
|
var n int
|
||||||
|
n = 0
|
||||||
|
c.Within(rect, 1,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 4)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Within(rect, 2,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 16)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Within(rect, 3,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 64)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Within(rect, 3,
|
||||||
|
func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
n++
|
||||||
|
return n <= 30
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 31)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Intersects(rect, 3,
|
||||||
|
func(id string, _ geojson.Object, _ []float64) bool {
|
||||||
|
n++
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 64)
|
||||||
|
|
||||||
|
n = 0
|
||||||
|
c.Intersects(rect, 3,
|
||||||
|
func(id string, _ geojson.Object, _ []float64) bool {
|
||||||
|
n++
|
||||||
|
return n <= 30
|
||||||
|
},
|
||||||
|
)
|
||||||
|
expect(t, n == 31)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCollectionVerifyContents(t *testing.T, c *Collection, objs map[string]geojson.Object) {
|
||||||
|
for id, o2 := range objs {
|
||||||
|
o1, _, ok := c.Get(id)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("ok[%s] = false, expect true", id)
|
||||||
|
}
|
||||||
|
j1 := string(o1.AppendJSON(nil))
|
||||||
|
j2 := string(o2.AppendJSON(nil))
|
||||||
|
if j1 != j2 {
|
||||||
|
t.Fatalf("j1 == %s, expect %s", j1, j2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManyCollections(t *testing.T) {
|
||||||
|
colsM := make(map[string]*Collection)
|
||||||
|
cols := 100
|
||||||
|
objs := 1000
|
||||||
|
k := 0
|
||||||
|
for i := 0; i < cols; i++ {
|
||||||
|
key := strconv.FormatInt(int64(i), 10)
|
||||||
|
for j := 0; j < objs; j++ {
|
||||||
|
id := strconv.FormatInt(int64(j), 10)
|
||||||
|
p := geometry.Point{
|
||||||
|
X: rand.Float64()*360 - 180,
|
||||||
|
Y: rand.Float64()*180 - 90,
|
||||||
|
}
|
||||||
|
obj := geojson.Object(PO(p.X, p.Y))
|
||||||
|
col, ok := colsM[key]
|
||||||
|
if !ok {
|
||||||
|
col = New()
|
||||||
|
colsM[key] = col
|
||||||
|
}
|
||||||
|
col.Set(id, obj, nil, nil)
|
||||||
|
k++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
col := colsM["13"]
|
||||||
|
//println(col.Count())
|
||||||
|
bbox := geometry.Rect{
|
||||||
|
Min: geometry.Point{X: -180, Y: 30},
|
||||||
|
Max: geometry.Point{X: 34, Y: 100},
|
||||||
|
}
|
||||||
|
col.geoSearch(bbox, func(id string, obj geojson.Object, fields []float64) bool {
|
||||||
|
//println(id)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type testPointItem struct {
|
||||||
|
id string
|
||||||
|
object geojson.Object
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkInsert(t *testing.B) {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
items := make([]testPointItem, t.N)
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
items[i] = testPointItem{
|
||||||
|
fmt.Sprintf("%d", i),
|
||||||
|
PO(rand.Float64()*360-180, rand.Float64()*180-90),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
col := New()
|
||||||
|
t.ResetTimer()
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
col.Set(items[i].id, items[i].object, nil, nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkReplace(t *testing.B) {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
items := make([]testPointItem, t.N)
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
items[i] = testPointItem{
|
||||||
|
fmt.Sprintf("%d", i),
|
||||||
|
PO(rand.Float64()*360-180, rand.Float64()*180-90),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
col := New()
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
col.Set(items[i].id, items[i].object, nil, nil)
|
||||||
|
}
|
||||||
|
t.ResetTimer()
|
||||||
|
for _, i := range rand.Perm(t.N) {
|
||||||
|
o, _, _ := col.Set(items[i].id, items[i].object, nil, nil)
|
||||||
|
if o != items[i].object {
|
||||||
|
t.Fatal("shoot!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkGet(t *testing.B) {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
items := make([]testPointItem, t.N)
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
items[i] = testPointItem{
|
||||||
|
fmt.Sprintf("%d", i),
|
||||||
|
PO(rand.Float64()*360-180, rand.Float64()*180-90),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
col := New()
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
col.Set(items[i].id, items[i].object, nil, nil)
|
||||||
|
}
|
||||||
|
t.ResetTimer()
|
||||||
|
for _, i := range rand.Perm(t.N) {
|
||||||
|
o, _, _ := col.Get(items[i].id)
|
||||||
|
if o != items[i].object {
|
||||||
|
t.Fatal("shoot!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkRemove(t *testing.B) {
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
items := make([]testPointItem, t.N)
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
items[i] = testPointItem{
|
||||||
|
fmt.Sprintf("%d", i),
|
||||||
|
PO(rand.Float64()*360-180, rand.Float64()*180-90),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
col := New()
|
||||||
|
for i := 0; i < t.N; i++ {
|
||||||
|
col.Set(items[i].id, items[i].object, nil, nil)
|
||||||
|
}
|
||||||
|
t.ResetTimer()
|
||||||
|
for _, i := range rand.Perm(t.N) {
|
||||||
|
o, _, _ := col.Delete(items[i].id)
|
||||||
|
if o != items[i].object {
|
||||||
|
t.Fatal("shoot!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package collection
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// String ...
|
||||||
|
type String string
|
||||||
|
|
||||||
|
var _ geojson.Object = String("")
|
||||||
|
|
||||||
|
// Spatial ...
|
||||||
|
func (s String) Spatial() geojson.Spatial {
|
||||||
|
return geojson.EmptySpatial{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEach ...
|
||||||
|
func (s String) ForEach(iter func(geom geojson.Object) bool) bool {
|
||||||
|
return iter(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty ...
|
||||||
|
func (s String) Empty() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rect ...
|
||||||
|
func (s String) Rect() geometry.Rect {
|
||||||
|
return geometry.Rect{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center ...
|
||||||
|
func (s String) Center() geometry.Point {
|
||||||
|
return geometry.Point{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppendJSON ...
|
||||||
|
func (s String) AppendJSON(dst []byte) []byte {
|
||||||
|
data, _ := json.Marshal(string(s))
|
||||||
|
return append(dst, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// String ...
|
||||||
|
func (s String) String() string {
|
||||||
|
return string(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JSON ...
|
||||||
|
func (s String) JSON() string {
|
||||||
|
return string(s.AppendJSON(nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Within ...
|
||||||
|
func (s String) Within(obj geojson.Object) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains ...
|
||||||
|
func (s String) Contains(obj geojson.Object) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersects ...
|
||||||
|
func (s String) Intersects(obj geojson.Object) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumPoints ...
|
||||||
|
func (s String) NumPoints() int {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance ...
|
||||||
|
func (s String) Distance(obj geojson.Object) float64 {
|
||||||
|
return 0
|
||||||
|
}
|
|
@ -14,8 +14,8 @@ import (
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
"github.com/tidwall/redcon"
|
"github.com/tidwall/redcon"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AsyncHooks indicates that the hooks should happen in the background.
|
// AsyncHooks indicates that the hooks should happen in the background.
|
|
@ -10,7 +10,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errCorruptedAOF = errors.New("corrupted aof file")
|
var errCorruptedAOF = errors.New("corrupted aof file")
|
|
@ -8,10 +8,10 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/tile38/pkg/collection"
|
"github.com/tidwall/tile38/core"
|
||||||
"github.com/tidwall/tile38/pkg/core"
|
"github.com/tidwall/tile38/internal/collection"
|
||||||
"github.com/tidwall/tile38/pkg/geojson"
|
"github.com/tidwall/geojson"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxkeys = 8
|
const maxkeys = 8
|
||||||
|
@ -127,20 +127,13 @@ func (c *Controller) aofshrink() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch obj := obj.(type) {
|
if objIsSpatial(obj) {
|
||||||
default:
|
|
||||||
if obj.IsGeometry() {
|
|
||||||
values = append(values, "object")
|
values = append(values, "object")
|
||||||
values = append(values, obj.JSON())
|
values = append(values, string(obj.AppendJSON(nil)))
|
||||||
} else {
|
} else {
|
||||||
values = append(values, "string")
|
values = append(values, "string")
|
||||||
values = append(values, obj.String())
|
values = append(values, obj.String())
|
||||||
}
|
}
|
||||||
case geojson.SimplePoint:
|
|
||||||
values = append(values, "point")
|
|
||||||
values = append(values, strconv.FormatFloat(obj.Y, 'f', -1, 64))
|
|
||||||
values = append(values, strconv.FormatFloat(obj.X, 'f', -1, 64))
|
|
||||||
}
|
|
||||||
|
|
||||||
// append the values to the aof buffer
|
// append the values to the aof buffer
|
||||||
aofbuf = append(aofbuf, '*')
|
aofbuf = append(aofbuf, '*')
|
|
@ -9,8 +9,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/core"
|
"github.com/tidwall/tile38/core"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// checksum performs a simple md5 checksum on the aof file
|
// checksum performs a simple md5 checksum on the aof file
|
|
@ -9,7 +9,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Conn represents a simple resp connection.
|
// Conn represents a simple resp connection.
|
|
@ -12,8 +12,8 @@ import (
|
||||||
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/glob"
|
"github.com/tidwall/tile38/internal/glob"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -21,6 +21,7 @@ const (
|
||||||
defaultProtectedMode = "yes"
|
defaultProtectedMode = "yes"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config keys
|
||||||
const (
|
const (
|
||||||
FollowHost = "follow_host"
|
FollowHost = "follow_host"
|
||||||
FollowPort = "follow_port"
|
FollowPort = "follow_port"
|
|
@ -18,14 +18,14 @@ import (
|
||||||
|
|
||||||
"github.com/tidwall/btree"
|
"github.com/tidwall/btree"
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/collection"
|
"github.com/tidwall/tile38/core"
|
||||||
"github.com/tidwall/tile38/pkg/core"
|
"github.com/tidwall/tile38/internal/collection"
|
||||||
"github.com/tidwall/tile38/pkg/endpoint"
|
"github.com/tidwall/tile38/internal/endpoint"
|
||||||
"github.com/tidwall/tile38/pkg/expire"
|
"github.com/tidwall/tile38/internal/expire"
|
||||||
"github.com/tidwall/tile38/pkg/geojson"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var errOOM = errors.New("OOM command not allowed when used memory > 'maxmemory'")
|
var errOOM = errors.New("OOM command not allowed when used memory > 'maxmemory'")
|
||||||
|
@ -121,6 +121,8 @@ type Controller struct {
|
||||||
func ListenAndServe(host string, port int, dir string, http bool) error {
|
func ListenAndServe(host string, port int, dir string, http bool) error {
|
||||||
return ListenAndServeEx(host, port, dir, nil, http)
|
return ListenAndServeEx(host, port, dir, nil, http)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListenAndServeEx ...
|
||||||
func ListenAndServeEx(host string, port int, dir string, ln *net.Listener, http bool) error {
|
func ListenAndServeEx(host string, port int, dir string, ln *net.Listener, http bool) error {
|
||||||
if core.AppendFileName == "" {
|
if core.AppendFileName == "" {
|
||||||
core.AppendFileName = path.Join(dir, "appendonly.aof")
|
core.AppendFileName = path.Join(dir, "appendonly.aof")
|
||||||
|
@ -155,8 +157,8 @@ func ListenAndServeEx(host string, port int, dir string, ln *net.Listener, http
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.epc = endpoint.NewManager(c)
|
c.epc = endpoint.NewManager(c)
|
||||||
c.luascripts = c.NewScriptMap()
|
c.luascripts = c.newScriptMap()
|
||||||
c.luapool = c.NewPool()
|
c.luapool = c.newPool()
|
||||||
defer c.luapool.Shutdown()
|
defer c.luapool.Shutdown()
|
||||||
|
|
||||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
if err := os.MkdirAll(dir, 0700); err != nil {
|
|
@ -7,13 +7,14 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mmcloughlin/geohash"
|
||||||
"github.com/tidwall/btree"
|
"github.com/tidwall/btree"
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/collection"
|
"github.com/tidwall/tile38/internal/collection"
|
||||||
"github.com/tidwall/tile38/pkg/geojson"
|
"github.com/tidwall/tile38/internal/glob"
|
||||||
"github.com/tidwall/tile38/pkg/geojson/geohash"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
"github.com/tidwall/tile38/pkg/glob"
|
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type fvt struct {
|
type fvt struct {
|
||||||
|
@ -76,10 +77,13 @@ func (c *Controller) cmdBounds(msg *server.Message) (resp.Value, error) {
|
||||||
}
|
}
|
||||||
minX, minY, maxX, maxY := col.Bounds()
|
minX, minY, maxX, maxY := col.Bounds()
|
||||||
|
|
||||||
bbox := geojson.New2DBBox(minX, minY, maxX, maxY)
|
bbox := geojson.NewRect(geometry.Rect{
|
||||||
|
Min: geometry.Point{X: minX, Y: minY},
|
||||||
|
Max: geometry.Point{X: maxX, Y: maxY},
|
||||||
|
})
|
||||||
if msg.OutputType == server.JSON {
|
if msg.OutputType == server.JSON {
|
||||||
buf.WriteString(`,"bounds":`)
|
buf.WriteString(`,"bounds":`)
|
||||||
buf.WriteString(bbox.ExternalJSON())
|
buf.WriteString(string(bbox.AppendJSON(nil)))
|
||||||
} else {
|
} else {
|
||||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||||
resp.ArrayValue([]resp.Value{
|
resp.ArrayValue([]resp.Value{
|
||||||
|
@ -182,21 +186,25 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) {
|
||||||
case "object":
|
case "object":
|
||||||
if msg.OutputType == server.JSON {
|
if msg.OutputType == server.JSON {
|
||||||
buf.WriteString(`,"object":`)
|
buf.WriteString(`,"object":`)
|
||||||
buf.WriteString(o.JSON())
|
buf.WriteString(string(o.AppendJSON(nil)))
|
||||||
} else {
|
} else {
|
||||||
vals = append(vals, resp.StringValue(o.String()))
|
vals = append(vals, resp.StringValue(o.String()))
|
||||||
}
|
}
|
||||||
case "point":
|
case "point":
|
||||||
point := o.CalculatedPoint()
|
|
||||||
if msg.OutputType == server.JSON {
|
if msg.OutputType == server.JSON {
|
||||||
buf.WriteString(`,"point":`)
|
buf.WriteString(`,"point":`)
|
||||||
buf.WriteString(point.ExternalJSON())
|
buf.Write(appendJSONSimplePoint(nil, o))
|
||||||
} else {
|
} else {
|
||||||
if point.Z != 0 {
|
point := o.Center()
|
||||||
|
var z float64
|
||||||
|
if gPoint, ok := o.(*geojson.Point); ok {
|
||||||
|
z = gPoint.Z()
|
||||||
|
}
|
||||||
|
if z != 0 {
|
||||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||||
resp.StringValue(strconv.FormatFloat(point.Y, 'f', -1, 64)),
|
resp.StringValue(strconv.FormatFloat(point.Y, 'f', -1, 64)),
|
||||||
resp.StringValue(strconv.FormatFloat(point.X, 'f', -1, 64)),
|
resp.StringValue(strconv.FormatFloat(point.X, 'f', -1, 64)),
|
||||||
resp.StringValue(strconv.FormatFloat(point.Z, 'f', -1, 64)),
|
resp.StringValue(strconv.FormatFloat(z, 'f', -1, 64)),
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||||
|
@ -216,21 +224,19 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) {
|
||||||
if err != nil || precision < 1 || precision > 64 {
|
if err != nil || precision < 1 || precision > 64 {
|
||||||
return server.NOMessage, errInvalidArgument(sprecision)
|
return server.NOMessage, errInvalidArgument(sprecision)
|
||||||
}
|
}
|
||||||
p, err := o.Geohash(int(precision))
|
center := o.Center()
|
||||||
if err != nil {
|
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(precision))
|
||||||
return server.NOMessage, err
|
|
||||||
}
|
|
||||||
if msg.OutputType == server.JSON {
|
if msg.OutputType == server.JSON {
|
||||||
buf.WriteString(`"` + p + `"`)
|
buf.WriteString(`"` + p + `"`)
|
||||||
} else {
|
} else {
|
||||||
vals = append(vals, resp.StringValue(p))
|
vals = append(vals, resp.StringValue(p))
|
||||||
}
|
}
|
||||||
case "bounds":
|
case "bounds":
|
||||||
bbox := o.CalculatedBBox()
|
|
||||||
if msg.OutputType == server.JSON {
|
if msg.OutputType == server.JSON {
|
||||||
buf.WriteString(`,"bounds":`)
|
buf.WriteString(`,"bounds":`)
|
||||||
buf.WriteString(bbox.ExternalJSON())
|
buf.Write(appendJSONSimpleBounds(nil, o))
|
||||||
} else {
|
} else {
|
||||||
|
bbox := o.Rect()
|
||||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||||
resp.ArrayValue([]resp.Value{
|
resp.ArrayValue([]resp.Value{
|
||||||
resp.FloatValue(bbox.Min.Y),
|
resp.FloatValue(bbox.Min.Y),
|
||||||
|
@ -582,7 +588,7 @@ func (c *Controller) parseSetArgs(vs []resp.Value) (
|
||||||
err = errInvalidNumberOfArguments
|
err = errInvalidNumberOfArguments
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.obj = geojson.String(str)
|
d.obj = collection.String(str)
|
||||||
case lcb(typ, "point"):
|
case lcb(typ, "point"):
|
||||||
var slat, slon, sz string
|
var slat, slon, sz string
|
||||||
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
|
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
|
||||||
|
@ -595,36 +601,36 @@ func (c *Controller) parseSetArgs(vs []resp.Value) (
|
||||||
}
|
}
|
||||||
vs, sz, ok = tokenval(vs)
|
vs, sz, ok = tokenval(vs)
|
||||||
if !ok || sz == "" {
|
if !ok || sz == "" {
|
||||||
var sp geojson.SimplePoint
|
var x, y float64
|
||||||
sp.Y, err = strconv.ParseFloat(slat, 64)
|
y, err = strconv.ParseFloat(slat, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errInvalidArgument(slat)
|
err = errInvalidArgument(slat)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sp.X, err = strconv.ParseFloat(slon, 64)
|
x, err = strconv.ParseFloat(slon, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errInvalidArgument(slon)
|
err = errInvalidArgument(slon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.obj = sp
|
d.obj = geojson.NewPoint(geometry.Point{X: x, Y: y})
|
||||||
} else {
|
} else {
|
||||||
var sp geojson.Point
|
var x, y, z float64
|
||||||
sp.Coordinates.Y, err = strconv.ParseFloat(slat, 64)
|
y, err = strconv.ParseFloat(slat, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errInvalidArgument(slat)
|
err = errInvalidArgument(slat)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sp.Coordinates.X, err = strconv.ParseFloat(slon, 64)
|
x, err = strconv.ParseFloat(slon, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errInvalidArgument(slon)
|
err = errInvalidArgument(slon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
sp.Coordinates.Z, err = strconv.ParseFloat(sz, 64)
|
z, err = strconv.ParseFloat(sz, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = errInvalidArgument(sz)
|
err = errInvalidArgument(sz)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.obj = sp
|
d.obj = geojson.NewPointZ(geometry.Point{X: x, Y: y}, z)
|
||||||
}
|
}
|
||||||
case lcb(typ, "bounds"):
|
case lcb(typ, "bounds"):
|
||||||
var sminlat, sminlon, smaxlat, smaxlon string
|
var sminlat, sminlon, smaxlat, smaxlon string
|
||||||
|
@ -665,40 +671,25 @@ func (c *Controller) parseSetArgs(vs []resp.Value) (
|
||||||
err = errInvalidArgument(smaxlon)
|
err = errInvalidArgument(smaxlon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
g := geojson.Polygon{
|
d.obj = geojson.NewRect(geometry.Rect{
|
||||||
Coordinates: [][]geojson.Position{
|
Min: geometry.Point{X: minlon, Y: minlat},
|
||||||
{
|
Max: geometry.Point{X: maxlon, Y: maxlat},
|
||||||
{X: minlon, Y: minlat, Z: 0},
|
})
|
||||||
{X: minlon, Y: maxlat, Z: 0},
|
|
||||||
{X: maxlon, Y: maxlat, Z: 0},
|
|
||||||
{X: maxlon, Y: minlat, Z: 0},
|
|
||||||
{X: minlon, Y: minlat, Z: 0},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
d.obj = g
|
|
||||||
case lcb(typ, "hash"):
|
case lcb(typ, "hash"):
|
||||||
var sp geojson.SimplePoint
|
|
||||||
var shash string
|
var shash string
|
||||||
if vs, shash, ok = tokenval(vs); !ok || shash == "" {
|
if vs, shash, ok = tokenval(vs); !ok || shash == "" {
|
||||||
err = errInvalidNumberOfArguments
|
err = errInvalidNumberOfArguments
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var lat, lon float64
|
lat, lon := geohash.Decode(shash)
|
||||||
lat, lon, err = geohash.Decode(shash)
|
d.obj = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sp.X = lon
|
|
||||||
sp.Y = lat
|
|
||||||
d.obj = sp
|
|
||||||
case lcb(typ, "object"):
|
case lcb(typ, "object"):
|
||||||
var object string
|
var object string
|
||||||
if vs, object, ok = tokenval(vs); !ok || object == "" {
|
if vs, object, ok = tokenval(vs); !ok || object == "" {
|
||||||
err = errInvalidNumberOfArguments
|
err = errInvalidNumberOfArguments
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
d.obj, err = geojson.ObjectJSON(object)
|
d.obj, err = geojson.Parse(object, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -832,7 +823,7 @@ func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDeta
|
||||||
var fields []string
|
var fields []string
|
||||||
var values []float64
|
var values []float64
|
||||||
var xx bool
|
var xx bool
|
||||||
var updated_count int
|
var updateCount int
|
||||||
d, fields, values, xx, err = c.parseFSetArgs(vs)
|
d, fields, values, xx, err = c.parseFSetArgs(vs)
|
||||||
|
|
||||||
col := c.getCol(d.key)
|
col := c.getCol(d.key)
|
||||||
|
@ -841,7 +832,7 @@ func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDeta
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var ok bool
|
var ok bool
|
||||||
d.obj, d.fields, updated_count, ok = col.SetFields(d.id, fields, values)
|
d.obj, d.fields, updateCount, ok = col.SetFields(d.id, fields, values)
|
||||||
if !(ok || xx) {
|
if !(ok || xx) {
|
||||||
err = errIDNotFound
|
err = errIDNotFound
|
||||||
return
|
return
|
||||||
|
@ -849,7 +840,7 @@ func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDeta
|
||||||
if ok {
|
if ok {
|
||||||
d.command = "fset"
|
d.command = "fset"
|
||||||
d.timestamp = time.Now()
|
d.timestamp = time.Now()
|
||||||
d.updated = updated_count > 0
|
d.updated = updateCount > 0
|
||||||
fmap := col.FieldMap()
|
fmap := col.FieldMap()
|
||||||
d.fmap = make(map[string]int)
|
d.fmap = make(map[string]int)
|
||||||
for key, idx := range fmap {
|
for key, idx := range fmap {
|
||||||
|
@ -861,7 +852,7 @@ func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDeta
|
||||||
case server.JSON:
|
case server.JSON:
|
||||||
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}")
|
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}")
|
||||||
case server.RESP:
|
case server.RESP:
|
||||||
res = resp.IntegerValue(updated_count)
|
res = resp.IntegerValue(updateCount)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
|
@ -10,8 +10,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MASSINSERT num_keys num_points [minx miny maxx maxy]
|
// MASSINSERT num_keys num_points [minx miny maxx maxy]
|
|
@ -7,7 +7,7 @@ import (
|
||||||
|
|
||||||
"github.com/tidwall/btree"
|
"github.com/tidwall/btree"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
type exitem struct {
|
type exitem struct {
|
|
@ -5,10 +5,11 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/tile38/pkg/geojson"
|
"github.com/tidwall/tile38/internal/glob"
|
||||||
"github.com/tidwall/tile38/pkg/glob"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// FenceMatch executes a fence match returns back json messages for fence detection.
|
// FenceMatch executes a fence match returns back json messages for fence detection.
|
||||||
|
@ -44,6 +45,12 @@ func appendHookDetails(b []byte, hookName string, metas []FenceMeta) []byte {
|
||||||
}
|
}
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func objIsSpatial(obj geojson.Object) bool {
|
||||||
|
_, ok := obj.(geojson.Spatial)
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
func hookJSONString(hookName string, metas []FenceMeta) string {
|
func hookJSONString(hookName string, metas []FenceMeta) string {
|
||||||
return string(appendHookDetails(nil, hookName, metas))
|
return string(appendHookDetails(nil, hookName, metas))
|
||||||
}
|
}
|
||||||
|
@ -63,7 +70,7 @@ func fenceMatch(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if details.obj == nil || !details.obj.IsGeometry() {
|
if details.obj == nil || !objIsSpatial(details.obj) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if details.command == "fset" {
|
if details.command == "fset" {
|
||||||
|
@ -120,12 +127,11 @@ func fenceMatch(
|
||||||
// Maybe the old object and new object create a line that crosses the fence.
|
// Maybe the old object and new object create a line that crosses the fence.
|
||||||
// Must detect for that possibility.
|
// Must detect for that possibility.
|
||||||
if details.oldObj != nil {
|
if details.oldObj != nil {
|
||||||
ls := geojson.LineString{
|
ls := geojson.NewLineString(geometry.NewLine(
|
||||||
Coordinates: []geojson.Position{
|
[]geometry.Point{
|
||||||
details.oldObj.CalculatedPoint(),
|
details.oldObj.Center(),
|
||||||
details.obj.CalculatedPoint(),
|
details.obj.Center(),
|
||||||
},
|
}, geometry.DefaultIndex))
|
||||||
}
|
|
||||||
temp := false
|
temp := false
|
||||||
if fence.cmd == "within" {
|
if fence.cmd == "within" {
|
||||||
// because we are testing if the line croses the area we need to use
|
// because we are testing if the line croses the area we need to use
|
||||||
|
@ -165,7 +171,7 @@ func fenceMatch(
|
||||||
sw.mu.Lock()
|
sw.mu.Lock()
|
||||||
var distance float64
|
var distance float64
|
||||||
if fence.distance {
|
if fence.distance {
|
||||||
distance = details.obj.CalculatedPoint().DistanceTo(geojson.Position{X: fence.lon, Y: fence.lat, Z: 0})
|
distance = details.obj.Distance(fence.obj)
|
||||||
}
|
}
|
||||||
sw.fmap = details.fmap
|
sw.fmap = details.fmap
|
||||||
sw.fullFields = true
|
sw.fullFields = true
|
||||||
|
@ -260,7 +266,7 @@ func extendRoamMessage(
|
||||||
nmsg = append(nmsg, `,"id":`...)
|
nmsg = append(nmsg, `,"id":`...)
|
||||||
nmsg = appendJSONString(nmsg, match.id)
|
nmsg = appendJSONString(nmsg, match.id)
|
||||||
nmsg = append(nmsg, `,"object":`...)
|
nmsg = append(nmsg, `,"object":`...)
|
||||||
nmsg = append(nmsg, match.obj.JSON()...)
|
nmsg = match.obj.AppendJSON(nmsg)
|
||||||
nmsg = append(nmsg, `,"meters":`...)
|
nmsg = append(nmsg, `,"meters":`...)
|
||||||
nmsg = strconv.AppendFloat(nmsg,
|
nmsg = strconv.AppendFloat(nmsg,
|
||||||
math.Floor(match.meters*1000)/1000, 'f', -1, 64)
|
math.Floor(match.meters*1000)/1000, 'f', -1, 64)
|
||||||
|
@ -270,9 +276,11 @@ func extendRoamMessage(
|
||||||
if col != nil {
|
if col != nil {
|
||||||
obj, _, ok := col.Get(match.id)
|
obj, _, ok := col.Get(match.id)
|
||||||
if ok {
|
if ok {
|
||||||
nmsg = append(nmsg,
|
nmsg = append(nmsg, `{"id":`...)
|
||||||
`{"id":`+jsonString(match.id)+
|
nmsg = appendJSONString(nmsg, match.id)
|
||||||
`,"self":true,"object":`+obj.JSON()+`}`...)
|
nmsg = append(nmsg, `,"self":true,"object":`...)
|
||||||
|
nmsg = obj.AppendJSON(nmsg)
|
||||||
|
nmsg = append(nmsg, '}')
|
||||||
}
|
}
|
||||||
pattern := match.id + fence.roam.scan
|
pattern := match.id + fence.roam.scan
|
||||||
iterator := func(
|
iterator := func(
|
||||||
|
@ -282,9 +290,11 @@ func extendRoamMessage(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if matched, _ := glob.Match(pattern, oid); matched {
|
if matched, _ := glob.Match(pattern, oid); matched {
|
||||||
nmsg = append(nmsg,
|
nmsg = append(nmsg, `,{"id":`...)
|
||||||
`,{"id":`+jsonString(oid)+
|
nmsg = appendJSONString(nmsg, oid)
|
||||||
`,"object":`+o.JSON()+`}`...)
|
nmsg = append(nmsg, `,"object":`...)
|
||||||
|
nmsg = o.AppendJSON(nmsg)
|
||||||
|
nmsg = append(nmsg, '}')
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -323,34 +333,22 @@ func makemsg(
|
||||||
}
|
}
|
||||||
|
|
||||||
func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool {
|
func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool {
|
||||||
if obj == nil {
|
gobj, _ := obj.(geojson.Object)
|
||||||
|
if gobj == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if fence.roam.on {
|
if fence.roam.on {
|
||||||
// we need to check this object against
|
// we need to check this object against
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
switch fence.cmd {
|
||||||
if fence.cmd == "nearby" {
|
case "nearby":
|
||||||
return obj.Nearby(geojson.Position{X: fence.lon, Y: fence.lat, Z: 0}, fence.meters)
|
// nearby is an INTERSECT on a Circle
|
||||||
}
|
return gobj.Intersects(fence.obj)
|
||||||
if fence.cmd == "within" {
|
case "within":
|
||||||
if fence.o != nil {
|
return gobj.Within(fence.obj)
|
||||||
return obj.Within(fence.o)
|
case "intersects":
|
||||||
}
|
return gobj.Intersects(fence.obj)
|
||||||
return obj.WithinBBox(geojson.BBox{
|
|
||||||
Min: geojson.Position{X: fence.minLon, Y: fence.minLat, Z: 0},
|
|
||||||
Max: geojson.Position{X: fence.maxLon, Y: fence.maxLat, Z: 0},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if fence.cmd == "intersects" {
|
|
||||||
if fence.o != nil {
|
|
||||||
return obj.Intersects(fence.o)
|
|
||||||
}
|
|
||||||
return obj.IntersectsBBox(geojson.BBox{
|
|
||||||
Min: geojson.Position{X: fence.minLon, Y: fence.minLat, Z: 0},
|
|
||||||
Max: geojson.Position{X: fence.maxLon, Y: fence.maxLat, Z: 0},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -359,15 +357,17 @@ func fenceMatchRoam(
|
||||||
c *Controller, fence *liveFenceSwitches,
|
c *Controller, fence *liveFenceSwitches,
|
||||||
tkey, tid string, obj geojson.Object,
|
tkey, tid string, obj geojson.Object,
|
||||||
) (nearbys, faraways []roamMatch) {
|
) (nearbys, faraways []roamMatch) {
|
||||||
|
|
||||||
col := c.getCol(fence.roam.key)
|
col := c.getCol(fence.roam.key)
|
||||||
if col == nil {
|
if col == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
p := obj.CalculatedPoint()
|
|
||||||
prevNearbys := fence.roam.nearbys[tid]
|
prevNearbys := fence.roam.nearbys[tid]
|
||||||
var newNearbys map[string]bool
|
var newNearbys map[string]bool
|
||||||
col.Nearby(0, p.Y, p.X, fence.roam.meters, math.Inf(-1), math.Inf(+1),
|
|
||||||
func(id string, obj geojson.Object, fields []float64) bool {
|
col.Intersects(obj, 0, func(
|
||||||
|
id string, obj2 geojson.Object, fields []float64,
|
||||||
|
) bool {
|
||||||
if c.hasExpired(fence.roam.key, id) {
|
if c.hasExpired(fence.roam.key, id) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -391,25 +391,24 @@ func fenceMatchRoam(
|
||||||
if prev {
|
if prev {
|
||||||
delete(prevNearbys, id)
|
delete(prevNearbys, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
match := roamMatch{
|
match := roamMatch{
|
||||||
id: id,
|
id: id,
|
||||||
obj: obj,
|
obj: obj2,
|
||||||
meters: obj.CalculatedPoint().DistanceTo(p),
|
meters: obj.Distance(obj2),
|
||||||
}
|
}
|
||||||
if !prev || !fence.nodwell {
|
if !prev || !fence.nodwell {
|
||||||
// brand new "nearby"
|
// brand new "nearby"
|
||||||
nearbys = append(nearbys, match)
|
nearbys = append(nearbys, match)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
},
|
})
|
||||||
)
|
|
||||||
for id := range prevNearbys {
|
for id := range prevNearbys {
|
||||||
obj, _, ok := col.Get(id)
|
obj2, _, ok := col.Get(id)
|
||||||
if ok && !c.hasExpired(fence.roam.key, id) {
|
if ok && !c.hasExpired(fence.roam.key, id) {
|
||||||
faraways = append(faraways, roamMatch{
|
faraways = append(faraways, roamMatch{
|
||||||
id: id, obj: obj,
|
id: id,
|
||||||
meters: obj.CalculatedPoint().DistanceTo(p),
|
obj: obj2,
|
||||||
|
meters: obj.Distance(obj2),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -10,9 +10,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/core"
|
"github.com/tidwall/tile38/core"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errNoLongerFollowing = errors.New("no longer following")
|
var errNoLongerFollowing = errors.New("no longer following")
|
|
@ -11,10 +11,10 @@ import (
|
||||||
|
|
||||||
"github.com/tidwall/buntdb"
|
"github.com/tidwall/buntdb"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/endpoint"
|
"github.com/tidwall/tile38/internal/endpoint"
|
||||||
"github.com/tidwall/tile38/pkg/glob"
|
"github.com/tidwall/tile38/internal/glob"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
var hookLogSetDefaults = &buntdb.SetOptions{
|
var hookLogSetDefaults = &buntdb.SetOptions{
|
|
@ -7,12 +7,12 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
"github.com/tidwall/tile38/pkg/collection"
|
"github.com/tidwall/tile38/internal/collection"
|
||||||
"github.com/tidwall/tile38/pkg/geojson"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func appendJSONString(b []byte, s string) []byte {
|
func appendJSONString(b []byte, s string) []byte {
|
||||||
|
@ -41,6 +41,37 @@ func jsonString(s string) string {
|
||||||
b[len(b)-1] = '"'
|
b[len(b)-1] = '"'
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
func appendJSONSimpleBounds(dst []byte, o geojson.Object) []byte {
|
||||||
|
bbox := o.Rect()
|
||||||
|
dst = append(dst, `{"sw":{"lat":`...)
|
||||||
|
dst = strconv.AppendFloat(dst, bbox.Min.Y, 'f', -1, 64)
|
||||||
|
dst = append(dst, `,"lon":`...)
|
||||||
|
dst = strconv.AppendFloat(dst, bbox.Min.X, 'f', -1, 64)
|
||||||
|
dst = append(dst, `},"ne":{"lat":`...)
|
||||||
|
dst = strconv.AppendFloat(dst, bbox.Max.Y, 'f', -1, 64)
|
||||||
|
dst = append(dst, `,"lon":`...)
|
||||||
|
dst = strconv.AppendFloat(dst, bbox.Max.X, 'f', -1, 64)
|
||||||
|
dst = append(dst, `}}`...)
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendJSONSimplePoint(dst []byte, o geojson.Object) []byte {
|
||||||
|
point := o.Center()
|
||||||
|
var z float64
|
||||||
|
if gPoint, ok := o.(*geojson.Point); ok {
|
||||||
|
z = gPoint.Z()
|
||||||
|
}
|
||||||
|
dst = append(dst, `{"lat":`...)
|
||||||
|
dst = strconv.AppendFloat(dst, point.Y, 'f', -1, 64)
|
||||||
|
dst = append(dst, `,"lon":`...)
|
||||||
|
dst = strconv.AppendFloat(dst, point.X, 'f', -1, 64)
|
||||||
|
if z != 0 {
|
||||||
|
dst = append(dst, `,"z":`...)
|
||||||
|
dst = strconv.AppendFloat(dst, z, 'f', -1, 64)
|
||||||
|
}
|
||||||
|
dst = append(dst, '}')
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
func appendJSONTimeFormat(b []byte, t time.Time) []byte {
|
func appendJSONTimeFormat(b []byte, t time.Time) []byte {
|
||||||
b = append(b, '"')
|
b = append(b, '"')
|
||||||
|
@ -174,9 +205,7 @@ func (c *Controller) cmdJset(msg *server.Message) (res resp.Value, d commandDeta
|
||||||
var geoobj bool
|
var geoobj bool
|
||||||
o, _, ok := col.Get(id)
|
o, _, ok := col.Get(id)
|
||||||
if ok {
|
if ok {
|
||||||
if _, ok := o.(geojson.String); !ok {
|
geoobj = objIsSpatial(o)
|
||||||
geoobj = true
|
|
||||||
}
|
|
||||||
json = o.String()
|
json = o.String()
|
||||||
}
|
}
|
||||||
if raw {
|
if raw {
|
||||||
|
@ -208,7 +237,7 @@ func (c *Controller) cmdJset(msg *server.Message) (res resp.Value, d commandDeta
|
||||||
|
|
||||||
d.key = key
|
d.key = key
|
||||||
d.id = id
|
d.id = id
|
||||||
d.obj = geojson.String(json)
|
d.obj = collection.String(json)
|
||||||
d.timestamp = time.Now()
|
d.timestamp = time.Now()
|
||||||
d.updated = true
|
d.updated = true
|
||||||
|
|
||||||
|
@ -248,9 +277,7 @@ func (c *Controller) cmdJdel(msg *server.Message) (res resp.Value, d commandDeta
|
||||||
var geoobj bool
|
var geoobj bool
|
||||||
o, _, ok := col.Get(id)
|
o, _, ok := col.Get(id)
|
||||||
if ok {
|
if ok {
|
||||||
if _, ok := o.(geojson.String); !ok {
|
geoobj = objIsSpatial(o)
|
||||||
geoobj = true
|
|
||||||
}
|
|
||||||
json = o.String()
|
json = o.String()
|
||||||
}
|
}
|
||||||
njson, err := sjson.Delete(json, path)
|
njson, err := sjson.Delete(json, path)
|
||||||
|
@ -282,7 +309,7 @@ func (c *Controller) cmdJdel(msg *server.Message) (res resp.Value, d commandDeta
|
||||||
|
|
||||||
d.key = key
|
d.key = key
|
||||||
d.id = id
|
d.id = id
|
||||||
d.obj = geojson.String(json)
|
d.obj = collection.String(json)
|
||||||
d.timestamp = time.Now()
|
d.timestamp = time.Now()
|
||||||
d.updated = true
|
d.updated = true
|
||||||
|
|
|
@ -7,8 +7,8 @@ import (
|
||||||
|
|
||||||
"github.com/tidwall/btree"
|
"github.com/tidwall/btree"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/glob"
|
"github.com/tidwall/tile38/internal/glob"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Controller) cmdKeys(msg *server.Message) (res resp.Value, err error) {
|
func (c *Controller) cmdKeys(msg *server.Message) (res resp.Value, err error) {
|
|
@ -8,8 +8,8 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
type liveBuffer struct {
|
type liveBuffer struct {
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Controller) cmdOutput(msg *server.Message) (res resp.Value, err error) {
|
func (c *Controller) cmdOutput(msg *server.Message) (res resp.Value, err error) {
|
|
@ -11,8 +11,8 @@ import (
|
||||||
"github.com/tidwall/match"
|
"github.com/tidwall/match"
|
||||||
"github.com/tidwall/redcon"
|
"github.com/tidwall/redcon"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
|
@ -5,8 +5,8 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Controller) cmdReadOnly(msg *server.Message) (res resp.Value, err error) {
|
func (c *Controller) cmdReadOnly(msg *server.Message) (res resp.Value, err error) {
|
|
@ -6,9 +6,9 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/geojson"
|
"github.com/tidwall/geojson"
|
||||||
"github.com/tidwall/tile38/pkg/glob"
|
"github.com/tidwall/tile38/internal/glob"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Controller) cmdScanArgs(vs []resp.Value) (
|
func (c *Controller) cmdScanArgs(vs []resp.Value) (
|
|
@ -7,11 +7,13 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/mmcloughlin/geohash"
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/collection"
|
"github.com/tidwall/tile38/internal/clip"
|
||||||
"github.com/tidwall/tile38/pkg/geojson"
|
"github.com/tidwall/tile38/internal/collection"
|
||||||
"github.com/tidwall/tile38/pkg/glob"
|
"github.com/tidwall/tile38/internal/glob"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
const limitItems = 100
|
const limitItems = 100
|
||||||
|
@ -58,6 +60,7 @@ type scanWriter struct {
|
||||||
respOut resp.Value
|
respOut resp.Value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ScanWriterParams ...
|
||||||
type ScanWriterParams struct {
|
type ScanWriterParams struct {
|
||||||
id string
|
id string
|
||||||
o geojson.Object
|
o geojson.Object
|
||||||
|
@ -65,8 +68,7 @@ type ScanWriterParams struct {
|
||||||
distance float64
|
distance float64
|
||||||
noLock bool
|
noLock bool
|
||||||
ignoreGlobMatch bool
|
ignoreGlobMatch bool
|
||||||
clip bool
|
clip geojson.Object
|
||||||
clipbox geojson.BBox
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) newScanWriter(
|
func (c *Controller) newScanWriter(
|
||||||
|
@ -199,7 +201,9 @@ func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []fl
|
||||||
for _, where := range sw.wheres {
|
for _, where := range sw.wheres {
|
||||||
if where.field == "z" {
|
if where.field == "z" {
|
||||||
if !gotz {
|
if !gotz {
|
||||||
z = o.CalculatedPoint().Z
|
if point, ok := o.(*geojson.Point); ok {
|
||||||
|
z = point.Z()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !where.match(z) {
|
if !where.match(z) {
|
||||||
return
|
return
|
||||||
|
@ -253,7 +257,9 @@ func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []fl
|
||||||
for _, where := range sw.wheres {
|
for _, where := range sw.wheres {
|
||||||
if where.field == "z" {
|
if where.field == "z" {
|
||||||
if !gotz {
|
if !gotz {
|
||||||
z = o.CalculatedPoint().Z
|
if point, ok := o.(*geojson.Point); ok {
|
||||||
|
z = point.Z()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !where.match(z) {
|
if !where.match(z) {
|
||||||
return
|
return
|
||||||
|
@ -344,8 +350,8 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool {
|
||||||
if sw.output == outputCount {
|
if sw.output == outputCount {
|
||||||
return sw.count < sw.limit
|
return sw.count < sw.limit
|
||||||
}
|
}
|
||||||
if opts.clip {
|
if opts.clip != nil {
|
||||||
opts.o = opts.o.Clipped(opts.clipbox)
|
opts.o = clip.Clip(opts.o, opts.clip)
|
||||||
}
|
}
|
||||||
switch sw.msg.OutputType {
|
switch sw.msg.OutputType {
|
||||||
case server.JSON:
|
case server.JSON:
|
||||||
|
@ -392,23 +398,21 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool {
|
||||||
wr.WriteString(`{"id":` + jsonString(opts.id))
|
wr.WriteString(`{"id":` + jsonString(opts.id))
|
||||||
switch sw.output {
|
switch sw.output {
|
||||||
case outputObjects:
|
case outputObjects:
|
||||||
wr.WriteString(`,"object":` + opts.o.JSON())
|
wr.WriteString(`,"object":` + string(opts.o.AppendJSON(nil)))
|
||||||
case outputPoints:
|
case outputPoints:
|
||||||
wr.WriteString(`,"point":` + opts.o.CalculatedPoint().ExternalJSON())
|
wr.WriteString(`,"point":` + string(appendJSONSimplePoint(nil, opts.o)))
|
||||||
case outputHashes:
|
case outputHashes:
|
||||||
p, err := opts.o.Geohash(int(sw.precision))
|
center := opts.o.Center()
|
||||||
if err != nil {
|
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(sw.precision))
|
||||||
p = ""
|
|
||||||
}
|
|
||||||
wr.WriteString(`,"hash":"` + p + `"`)
|
wr.WriteString(`,"hash":"` + p + `"`)
|
||||||
case outputBounds:
|
case outputBounds:
|
||||||
wr.WriteString(`,"bounds":` + opts.o.CalculatedBBox().ExternalJSON())
|
wr.WriteString(`,"bounds":` + string(appendJSONSimpleBounds(nil, opts.o)))
|
||||||
}
|
}
|
||||||
|
|
||||||
wr.WriteString(jsfields)
|
wr.WriteString(jsfields)
|
||||||
|
|
||||||
if opts.distance > 0 {
|
if opts.distance > 0 {
|
||||||
wr.WriteString(`,"distance":` + strconv.FormatFloat(opts.distance, 'f', 2, 64))
|
wr.WriteString(`,"distance":` + strconv.FormatFloat(opts.distance, 'f', -1, 64))
|
||||||
}
|
}
|
||||||
|
|
||||||
wr.WriteString(`}`)
|
wr.WriteString(`}`)
|
||||||
|
@ -424,12 +428,16 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool {
|
||||||
case outputObjects:
|
case outputObjects:
|
||||||
vals = append(vals, resp.StringValue(opts.o.String()))
|
vals = append(vals, resp.StringValue(opts.o.String()))
|
||||||
case outputPoints:
|
case outputPoints:
|
||||||
point := opts.o.CalculatedPoint()
|
point := opts.o.Center()
|
||||||
if point.Z != 0 {
|
var z float64
|
||||||
|
if point, ok := opts.o.(*geojson.Point); ok {
|
||||||
|
z = point.Z()
|
||||||
|
}
|
||||||
|
if z != 0 {
|
||||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||||
resp.FloatValue(point.Y),
|
resp.FloatValue(point.Y),
|
||||||
resp.FloatValue(point.X),
|
resp.FloatValue(point.X),
|
||||||
resp.FloatValue(point.Z),
|
resp.FloatValue(z),
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||||
|
@ -438,13 +446,11 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
case outputHashes:
|
case outputHashes:
|
||||||
p, err := opts.o.Geohash(int(sw.precision))
|
center := opts.o.Center()
|
||||||
if err != nil {
|
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(sw.precision))
|
||||||
p = ""
|
|
||||||
}
|
|
||||||
vals = append(vals, resp.StringValue(p))
|
vals = append(vals, resp.StringValue(p))
|
||||||
case outputBounds:
|
case outputBounds:
|
||||||
bbox := opts.o.CalculatedBBox()
|
bbox := opts.o.Rect()
|
||||||
vals = append(vals, resp.ArrayValue([]resp.Value{
|
vals = append(vals, resp.ArrayValue([]resp.Value{
|
||||||
resp.ArrayValue([]resp.Value{
|
resp.ArrayValue([]resp.Value{
|
||||||
resp.FloatValue(bbox.Min.Y),
|
resp.FloatValue(bbox.Min.Y),
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
"github.com/yuin/gopher-lua"
|
"github.com/yuin/gopher-lua"
|
||||||
luajson "layeh.com/gopher-json"
|
luajson "layeh.com/gopher-json"
|
||||||
)
|
)
|
||||||
|
@ -39,8 +39,8 @@ type lStatePool struct {
|
||||||
total int
|
total int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPool returns a new pool of lua states
|
// newPool returns a new pool of lua states
|
||||||
func (c *Controller) NewPool() *lStatePool {
|
func (c *Controller) newPool() *lStatePool {
|
||||||
pl := &lStatePool{
|
pl := &lStatePool{
|
||||||
saved: make([]*lua.LState, iniLuaPoolSize),
|
saved: make([]*lua.LState, iniLuaPoolSize),
|
||||||
c: c,
|
c: c,
|
||||||
|
@ -206,7 +206,7 @@ func (sm *lScriptMap) Flush() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScriptMap returns a new map with lua scripts
|
// NewScriptMap returns a new map with lua scripts
|
||||||
func (c *Controller) NewScriptMap() *lScriptMap {
|
func (c *Controller) newScriptMap() *lScriptMap {
|
||||||
return &lScriptMap{
|
return &lScriptMap{
|
||||||
scripts: make(map[string]*lua.FunctionProto),
|
scripts: make(map[string]*lua.FunctionProto),
|
||||||
}
|
}
|
|
@ -8,20 +8,20 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/mmcloughlin/geohash"
|
||||||
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/bing"
|
"github.com/tidwall/tile38/internal/bing"
|
||||||
"github.com/tidwall/tile38/pkg/geojson"
|
"github.com/tidwall/tile38/internal/glob"
|
||||||
"github.com/tidwall/tile38/pkg/geojson/geohash"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
"github.com/tidwall/tile38/pkg/glob"
|
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const defaultCircleSteps = 64
|
||||||
|
|
||||||
type liveFenceSwitches struct {
|
type liveFenceSwitches struct {
|
||||||
searchScanBaseTokens
|
searchScanBaseTokens
|
||||||
lat, lon, meters float64
|
obj geojson.Object
|
||||||
o geojson.Object
|
|
||||||
minLat, minLon float64
|
|
||||||
maxLat, maxLon float64
|
|
||||||
cmd string
|
cmd string
|
||||||
roam roamSwitches
|
roam roamSwitches
|
||||||
knn bool
|
knn bool
|
||||||
|
@ -102,9 +102,14 @@ func (c *Controller) cmdSearchArgs(
|
||||||
err = errInvalidArgument(typ)
|
err = errInvalidArgument(typ)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.meters = -1 // this will become non-negative if search is within a circle
|
|
||||||
switch ltyp {
|
switch ltyp {
|
||||||
case "point":
|
case "point":
|
||||||
|
fallthrough
|
||||||
|
case "circle":
|
||||||
|
if s.clip {
|
||||||
|
err = errInvalidArgument("cannnot clip with " + ltyp)
|
||||||
|
return
|
||||||
|
}
|
||||||
var slat, slon, smeters string
|
var slat, slon, smeters string
|
||||||
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
|
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
|
||||||
err = errInvalidNumberOfArguments
|
err = errInvalidNumberOfArguments
|
||||||
|
@ -114,11 +119,6 @@ func (c *Controller) cmdSearchArgs(
|
||||||
err = errInvalidNumberOfArguments
|
err = errInvalidNumberOfArguments
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.clip {
|
|
||||||
err = errInvalidArgument("cannnot clip with point")
|
|
||||||
}
|
|
||||||
|
|
||||||
umeters := true
|
umeters := true
|
||||||
if vs, smeters, ok = tokenval(vs); !ok || smeters == "" {
|
if vs, smeters, ok = tokenval(vs); !ok || smeters == "" {
|
||||||
umeters = false
|
umeters = false
|
||||||
|
@ -132,72 +132,42 @@ func (c *Controller) cmdSearchArgs(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var lat, lon, meters float64
|
||||||
if s.lat, err = strconv.ParseFloat(slat, 64); err != nil {
|
if lat, err = strconv.ParseFloat(slat, 64); err != nil {
|
||||||
err = errInvalidArgument(slat)
|
err = errInvalidArgument(slat)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.lon, err = strconv.ParseFloat(slon, 64); err != nil {
|
if lon, err = strconv.ParseFloat(slon, 64); err != nil {
|
||||||
err = errInvalidArgument(slon)
|
err = errInvalidArgument(slon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if umeters {
|
if umeters {
|
||||||
if s.meters, err = strconv.ParseFloat(smeters, 64); err != nil {
|
if meters, err = strconv.ParseFloat(smeters, 64); err != nil {
|
||||||
err = errInvalidArgument(smeters)
|
err = errInvalidArgument(smeters)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.meters < 0 {
|
if meters < 0 {
|
||||||
err = errInvalidArgument(smeters)
|
err = errInvalidArgument(smeters)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "circle":
|
if s.knn {
|
||||||
if s.clip {
|
s.obj = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
|
||||||
err = errInvalidArgument("cannnot clip with circle")
|
} else {
|
||||||
}
|
s.obj = geojson.NewCircle(geometry.Point{X: lon, Y: lat},
|
||||||
|
meters, defaultCircleSteps)
|
||||||
var slat, slon, smeters string
|
|
||||||
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
|
|
||||||
err = errInvalidNumberOfArguments
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if vs, slon, ok = tokenval(vs); !ok || slon == "" {
|
|
||||||
err = errInvalidNumberOfArguments
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if vs, smeters, ok = tokenval(vs); !ok || smeters == "" {
|
|
||||||
err = errInvalidArgument(slat)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.lat, err = strconv.ParseFloat(slat, 64); err != nil {
|
|
||||||
err = errInvalidArgument(slat)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.lon, err = strconv.ParseFloat(slon, 64); err != nil {
|
|
||||||
err = errInvalidArgument(slon)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.meters, err = strconv.ParseFloat(smeters, 64); err != nil {
|
|
||||||
err = errInvalidArgument(smeters)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if s.meters < 0 {
|
|
||||||
err = errInvalidArgument(smeters)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
case "object":
|
case "object":
|
||||||
if s.clip {
|
if s.clip {
|
||||||
err = errInvalidArgument("cannnot clip with object")
|
err = errInvalidArgument("cannnot clip with object")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var obj string
|
var obj string
|
||||||
if vs, obj, ok = tokenval(vs); !ok || obj == "" {
|
if vs, obj, ok = tokenval(vs); !ok || obj == "" {
|
||||||
err = errInvalidNumberOfArguments
|
err = errInvalidNumberOfArguments
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.o, err = geojson.ObjectJSON(obj)
|
s.obj, err = geojson.Parse(obj, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -219,42 +189,54 @@ func (c *Controller) cmdSearchArgs(
|
||||||
err = errInvalidNumberOfArguments
|
err = errInvalidNumberOfArguments
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.minLat, err = strconv.ParseFloat(sminLat, 64); err != nil {
|
var minLat, minLon, maxLat, maxLon float64
|
||||||
|
if minLat, err = strconv.ParseFloat(sminLat, 64); err != nil {
|
||||||
err = errInvalidArgument(sminLat)
|
err = errInvalidArgument(sminLat)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.minLon, err = strconv.ParseFloat(sminLon, 64); err != nil {
|
if minLon, err = strconv.ParseFloat(sminLon, 64); err != nil {
|
||||||
err = errInvalidArgument(sminLon)
|
err = errInvalidArgument(sminLon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.maxLat, err = strconv.ParseFloat(smaxlat, 64); err != nil {
|
if maxLat, err = strconv.ParseFloat(smaxlat, 64); err != nil {
|
||||||
err = errInvalidArgument(smaxlat)
|
err = errInvalidArgument(smaxlat)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.maxLon, err = strconv.ParseFloat(smaxlon, 64); err != nil {
|
if maxLon, err = strconv.ParseFloat(smaxlon, 64); err != nil {
|
||||||
err = errInvalidArgument(smaxlon)
|
err = errInvalidArgument(smaxlon)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.obj = geojson.NewRect(geometry.Rect{
|
||||||
|
Min: geometry.Point{X: minLon, Y: minLat},
|
||||||
|
Max: geometry.Point{X: maxLon, Y: maxLat},
|
||||||
|
})
|
||||||
case "hash":
|
case "hash":
|
||||||
var hash string
|
var hash string
|
||||||
if vs, hash, ok = tokenval(vs); !ok || hash == "" {
|
if vs, hash, ok = tokenval(vs); !ok || hash == "" {
|
||||||
err = errInvalidNumberOfArguments
|
err = errInvalidNumberOfArguments
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.minLat, s.minLon, s.maxLat, s.maxLon, err = geohash.Bounds(hash); err != nil {
|
box := geohash.BoundingBox(hash)
|
||||||
err = errInvalidArgument(hash)
|
s.obj = geojson.NewRect(geometry.Rect{
|
||||||
return
|
Min: geometry.Point{X: box.MinLng, Y: box.MinLat},
|
||||||
}
|
Max: geometry.Point{X: box.MaxLng, Y: box.MaxLat},
|
||||||
|
})
|
||||||
case "quadkey":
|
case "quadkey":
|
||||||
var key string
|
var key string
|
||||||
if vs, key, ok = tokenval(vs); !ok || key == "" {
|
if vs, key, ok = tokenval(vs); !ok || key == "" {
|
||||||
err = errInvalidNumberOfArguments
|
err = errInvalidNumberOfArguments
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.minLat, s.minLon, s.maxLat, s.maxLon, err = bing.QuadKeyToBounds(key); err != nil {
|
var minLat, minLon, maxLat, maxLon float64
|
||||||
|
minLat, minLon, maxLat, maxLon, err = bing.QuadKeyToBounds(key)
|
||||||
|
if err != nil {
|
||||||
err = errInvalidArgument(key)
|
err = errInvalidArgument(key)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
s.obj = geojson.NewRect(geometry.Rect{
|
||||||
|
Min: geometry.Point{X: minLon, Y: minLat},
|
||||||
|
Max: geometry.Point{X: maxLon, Y: maxLat},
|
||||||
|
})
|
||||||
case "tile":
|
case "tile":
|
||||||
var sx, sy, sz string
|
var sx, sy, sz string
|
||||||
if vs, sx, ok = tokenval(vs); !ok || sx == "" {
|
if vs, sx, ok = tokenval(vs); !ok || sx == "" {
|
||||||
|
@ -283,7 +265,12 @@ func (c *Controller) cmdSearchArgs(
|
||||||
err = errInvalidArgument(sz)
|
err = errInvalidArgument(sz)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.minLat, s.minLon, s.maxLat, s.maxLon = bing.TileXYToBounds(x, y, z)
|
var minLat, minLon, maxLat, maxLon float64
|
||||||
|
minLat, minLon, maxLat, maxLon = bing.TileXYToBounds(x, y, z)
|
||||||
|
s.obj = geojson.NewRect(geometry.Rect{
|
||||||
|
Min: geometry.Point{X: minLon, Y: minLat},
|
||||||
|
Max: geometry.Point{X: maxLon, Y: maxLat},
|
||||||
|
})
|
||||||
case "get":
|
case "get":
|
||||||
if s.clip {
|
if s.clip {
|
||||||
err = errInvalidArgument("cannnot clip with get")
|
err = errInvalidArgument("cannnot clip with get")
|
||||||
|
@ -302,20 +289,11 @@ func (c *Controller) cmdSearchArgs(
|
||||||
err = errKeyNotFound
|
err = errKeyNotFound
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
o, _, ok := col.Get(id)
|
s.obj, _, ok = col.Get(id)
|
||||||
if !ok {
|
if !ok {
|
||||||
err = errIDNotFound
|
err = errIDNotFound
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if o.IsBBoxDefined() {
|
|
||||||
bbox := o.CalculatedBBox()
|
|
||||||
s.minLat = bbox.Min.Y
|
|
||||||
s.minLon = bbox.Min.X
|
|
||||||
s.maxLat = bbox.Max.Y
|
|
||||||
s.maxLon = bbox.Max.X
|
|
||||||
} else {
|
|
||||||
s.o = o
|
|
||||||
}
|
|
||||||
case "roam":
|
case "roam":
|
||||||
s.roam.on = true
|
s.roam.on = true
|
||||||
if vs, s.roam.key, ok = tokenval(vs); !ok || s.roam.key == "" {
|
if vs, s.roam.key, ok = tokenval(vs); !ok || s.roam.key == "" {
|
||||||
|
@ -336,7 +314,6 @@ func (c *Controller) cmdSearchArgs(
|
||||||
err = errInvalidArgument(smeters)
|
err = errInvalidArgument(smeters)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var scan string
|
var scan string
|
||||||
if vs, scan, ok = tokenval(vs); ok {
|
if vs, scan, ok = tokenval(vs); ok {
|
||||||
if strings.ToLower(scan) != "scan" {
|
if strings.ToLower(scan) != "scan" {
|
||||||
|
@ -383,7 +360,6 @@ func (c *Controller) cmdNearby(msg *server.Message) (res resp.Value, err error)
|
||||||
if s.fence {
|
if s.fence {
|
||||||
return server.NOMessage, s
|
return server.NOMessage, s
|
||||||
}
|
}
|
||||||
minZ, maxZ := zMinMaxFromWheres(s.wheres)
|
|
||||||
sw, err := c.newScanWriter(
|
sw, err := c.newScanWriter(
|
||||||
wr, msg, s.key, s.output, s.precision, s.glob, false,
|
wr, msg, s.key, s.output, s.precision, s.glob, false,
|
||||||
s.cursor, s.limit, s.wheres, s.whereins, s.whereevals, s.nofields)
|
s.cursor, s.limit, s.wheres, s.whereins, s.whereevals, s.nofields)
|
||||||
|
@ -403,7 +379,7 @@ func (c *Controller) cmdNearby(msg *server.Message) (res resp.Value, err error)
|
||||||
if dist != nil {
|
if dist != nil {
|
||||||
distance = *dist
|
distance = *dist
|
||||||
} else {
|
} else {
|
||||||
distance = o.CalculatedPoint().DistanceTo(geojson.Position{X: s.lon, Y: s.lat, Z: 0})
|
distance = o.Distance(s.obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sw.writeObject(ScanWriterParams{
|
return sw.writeObject(ScanWriterParams{
|
||||||
|
@ -416,16 +392,16 @@ func (c *Controller) cmdNearby(msg *server.Message) (res resp.Value, err error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if s.knn {
|
if s.knn {
|
||||||
c.nearestNeighbors(&s, sw, s.lat, s.lon, &matched, iter)
|
c.nearestNeighbors(&s, sw, s.obj, &matched, iter)
|
||||||
} else {
|
} else {
|
||||||
sw.col.Nearby(s.sparse, s.lat, s.lon, s.meters, minZ, maxZ,
|
sw.col.Intersects(s.obj, s.sparse, func(
|
||||||
func(id string, o geojson.Object, fields []float64) bool {
|
id string, o geojson.Object, fields []float64,
|
||||||
|
) bool {
|
||||||
if c.hasExpired(s.key, id) {
|
if c.hasExpired(s.key, id) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return iter(id, o, fields, nil)
|
return iter(id, o, fields, nil)
|
||||||
},
|
})
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sw.writeFoot()
|
sw.writeFoot()
|
||||||
|
@ -443,11 +419,13 @@ type iterItem struct {
|
||||||
dist float64
|
dist float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) nearestNeighbors(s *liveFenceSwitches, sw *scanWriter, lat, lon float64, matched *uint32,
|
func (c *Controller) nearestNeighbors(
|
||||||
iter func(id string, o geojson.Object, fields []float64, dist *float64) bool) {
|
s *liveFenceSwitches, sw *scanWriter, target geojson.Object, matched *uint32,
|
||||||
|
iter func(id string, o geojson.Object, fields []float64, dist *float64,
|
||||||
|
) bool) {
|
||||||
limit := int(sw.cursor + sw.limit)
|
limit := int(sw.cursor + sw.limit)
|
||||||
var items []iterItem
|
var items []iterItem
|
||||||
sw.col.NearestNeighbors(lat, lon, func(id string, o geojson.Object, fields []float64) bool {
|
sw.col.Nearby(target, func(id string, o geojson.Object, fields []float64) bool {
|
||||||
if c.hasExpired(s.key, id) {
|
if c.hasExpired(s.key, id) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -458,7 +436,7 @@ func (c *Controller) nearestNeighbors(s *liveFenceSwitches, sw *scanWriter, lat,
|
||||||
if !match {
|
if !match {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
dist := o.CalculatedPoint().DistanceTo(geojson.Position{X: lon, Y: lat, Z: 0})
|
dist := o.Distance(target)
|
||||||
items = append(items, iterItem{id: id, o: o, fields: fields, dist: dist})
|
items = append(items, iterItem{id: id, o: o, fields: fields, dist: dist})
|
||||||
if !keepGoing {
|
if !keepGoing {
|
||||||
return false
|
return false
|
||||||
|
@ -517,14 +495,10 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res
|
||||||
}
|
}
|
||||||
sw.writeHead()
|
sw.writeHead()
|
||||||
if sw.col != nil {
|
if sw.col != nil {
|
||||||
minZ, maxZ := zMinMaxFromWheres(s.wheres)
|
|
||||||
if cmd == "within" {
|
if cmd == "within" {
|
||||||
sw.col.Within(s.sparse,
|
sw.col.Within(s.obj, s.sparse, func(
|
||||||
s.o,
|
id string, o geojson.Object, fields []float64,
|
||||||
s.minLat, s.minLon, s.maxLat, s.maxLon,
|
) bool {
|
||||||
s.lat, s.lon, s.meters,
|
|
||||||
minZ, maxZ,
|
|
||||||
func(id string, o geojson.Object, fields []float64) bool {
|
|
||||||
if c.hasExpired(s.key, id) {
|
if c.hasExpired(s.key, id) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -534,29 +508,27 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res
|
||||||
fields: fields,
|
fields: fields,
|
||||||
noLock: true,
|
noLock: true,
|
||||||
})
|
})
|
||||||
},
|
})
|
||||||
)
|
|
||||||
} else if cmd == "intersects" {
|
} else if cmd == "intersects" {
|
||||||
sw.col.Intersects(s.sparse,
|
sw.col.Intersects(s.obj, s.sparse, func(
|
||||||
s.o,
|
id string,
|
||||||
s.minLat, s.minLon, s.maxLat, s.maxLon,
|
o geojson.Object,
|
||||||
s.lat, s.lon, s.meters,
|
fields []float64,
|
||||||
minZ, maxZ,
|
) bool {
|
||||||
s.clip,
|
|
||||||
func(id string, o geojson.Object, fields []float64, clipbox geojson.BBox) bool {
|
|
||||||
if c.hasExpired(s.key, id) {
|
if c.hasExpired(s.key, id) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return sw.writeObject(ScanWriterParams{
|
params := ScanWriterParams{
|
||||||
id: id,
|
id: id,
|
||||||
o: o,
|
o: o,
|
||||||
fields: fields,
|
fields: fields,
|
||||||
noLock: true,
|
noLock: true,
|
||||||
clip: s.clip,
|
}
|
||||||
clipbox: clipbox,
|
if s.clip {
|
||||||
|
params.clip = s.obj
|
||||||
|
}
|
||||||
|
return sw.writeObject(params)
|
||||||
})
|
})
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
sw.writeFoot()
|
sw.writeFoot()
|
|
@ -12,8 +12,8 @@ import (
|
||||||
|
|
||||||
"github.com/tidwall/btree"
|
"github.com/tidwall/btree"
|
||||||
"github.com/tidwall/resp"
|
"github.com/tidwall/resp"
|
||||||
"github.com/tidwall/tile38/pkg/core"
|
"github.com/tidwall/tile38/core"
|
||||||
"github.com/tidwall/tile38/pkg/server"
|
"github.com/tidwall/tile38/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Controller) cmdStats(msg *server.Message) (res resp.Value, err error) {
|
func (c *Controller) cmdStats(msg *server.Message) (res resp.Value, err error) {
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/garyburd/redigo/redis"
|
"github.com/garyburd/redigo/redis"
|
||||||
"github.com/tidwall/tile38/pkg/log"
|
"github.com/tidwall/tile38/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
|
@ -6,7 +6,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/tile38/pkg/hservice"
|
"github.com/tidwall/tile38/internal/hservice"
|
||||||
"golang.org/x/net/context"
|
"golang.org/x/net/context"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
)
|
)
|