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.
This commit is contained in:
tidwall 2018-10-10 14:25:40 -07:00
parent 5ad27e7b5e
commit 6257ddba78
370 changed files with 35937 additions and 42238 deletions

53
Gopkg.lock generated
View File

@ -133,6 +133,34 @@
pruneopts = ""
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]]
branch = "master"
digest = "1:30f72985e574101b71666d6e601e7564bd02d95164da59ca17363ad194137969"
@ -197,6 +225,18 @@
pruneopts = ""
revision = "b67b1b8c1658cb01502801c14e33c61e6c4cbb95"
[[projects]]
branch = "master"
digest = "1:157eb52179752d4d88f1049aa6c3e4954d6796af5f6bcd54b5ab40f8637805df"
name = "github.com/tidwall/geojson"
packages = [
".",
"geo",
"geometry",
]
pruneopts = ""
revision = "8ff3ef500c61617c9f325603cf40863ca7086a1d"
[[projects]]
digest = "1:211773b67c5594aa92b1e8389c59558fa4927614507ea38237265e00c0ba6b81"
name = "github.com/tidwall/gjson"
@ -229,6 +269,14 @@
pruneopts = ""
revision = "1731857f09b1f38450e2c12409748407822dc6be"
[[projects]]
branch = "master"
digest = "1:7eed51dcae60e95dbde54662594ef90a7cbf3b7e3f0de32f84f0213b695967ff"
name = "github.com/tidwall/pretty"
packages = ["."]
pruneopts = ""
revision = "65a9db5fad5105a89e17f38adcc9878685be6d78"
[[projects]]
branch = "master"
digest = "1:630381558bc538e831db8468dd0dc2702d81789f79b8ddf665eeebc729e2a055"
@ -394,13 +442,18 @@
"github.com/eclipse/paho.mqtt.golang",
"github.com/garyburd/redigo/redis",
"github.com/golang/protobuf/proto",
"github.com/mmcloughlin/geohash",
"github.com/nats-io/go-nats",
"github.com/peterh/liner",
"github.com/streadway/amqp",
"github.com/tidwall/boxtree/d2",
"github.com/tidwall/btree",
"github.com/tidwall/buntdb",
"github.com/tidwall/geojson",
"github.com/tidwall/geojson/geometry",
"github.com/tidwall/gjson",
"github.com/tidwall/lotsa",
"github.com/tidwall/match",
"github.com/tidwall/redbench",
"github.com/tidwall/redcon",
"github.com/tidwall/resp",

View File

@ -20,7 +20,11 @@
# name = "github.com/x/y"
# version = "2.4.0"
required = ["github.com/tidwall/lotsa"]
required = [
"github.com/tidwall/lotsa",
"github.com/mmcloughlin/geohash",
"github.com/tidwall/geojson"
]
[[constraint]]
branch = "master"
@ -50,10 +54,6 @@ required = ["github.com/tidwall/lotsa"]
branch = "master"
name = "github.com/streadway/amqp"
[[constraint]]
name = "github.com/stretchr/testify"
version = "1.1.4"
[[constraint]]
branch = "master"
name = "github.com/tidwall/btree"
@ -82,10 +82,6 @@ required = ["github.com/tidwall/lotsa"]
name = "github.com/tidwall/sjson"
version = "1.0.0"
[[constraint]]
branch = "master"
name = "github.com/tidwall/tinyqueue"
[[constraint]]
branch = "master"
name = "golang.org/x/crypto"

View File

@ -1,6 +1,6 @@
<p align="center">
<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>
</p>
<p align="center">
@ -14,11 +14,11 @@ Tile38 is an open source (MIT licensed), in-memory geolocation data store, spati
<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>
<a href="#searching"><img src="/pkg/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="/pkg/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/roaming-geofences"><img src="/pkg/assets/roaming.gif" alt="Roaming Geofences" 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="/internal/assets/search-within.png" alt="Within" 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="/internal/assets/geofence.gif" alt="Geofencing" 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>
## 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.
<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 searches a collection for objects that are fully contained inside a specified bounding area.
<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 searches a collection for objects that intersect a specified bounding area.
<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 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
<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.
*Tile38 also allows for [Webhooks](http://tile38.com/commands/sethook) to be assigned to Geofences.*

View File

@ -161,7 +161,7 @@ if [ "$NOLINK" != "1" ]; then
fi
# generate the core package
pkg/core/gen.sh
core/gen.sh
# build and store objects into original directory.
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
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
# cover if requested

View File

@ -11,7 +11,7 @@ import (
"time"
"github.com/tidwall/redbench"
"github.com/tidwall/tile38/pkg/core"
"github.com/tidwall/tile38/core"
)
var (

View File

@ -16,8 +16,8 @@ import (
"github.com/peterh/liner"
"github.com/tidwall/gjson"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/client"
"github.com/tidwall/tile38/pkg/core"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/client"
)
func userHomeDir() string {

View File

@ -15,14 +15,12 @@ import (
"sync"
"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"
"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 (

View File

Before

Width:  |  Height:  |  Size: 139 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 985 KiB

After

Width:  |  Height:  |  Size: 985 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

View File

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

View File

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 110 KiB

View File

Before

Width:  |  Height:  |  Size: 252 KiB

After

Width:  |  Height:  |  Size: 252 KiB

View File

Before

Width:  |  Height:  |  Size: 269 KiB

After

Width:  |  Height:  |  Size: 269 KiB

View File

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 196 KiB

142
internal/clip/clip.go Normal file
View File

@ -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
}

View File

@ -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())
// }

View File

@ -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)
}

13
internal/clip/feature.go Normal file
View File

@ -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
}

View File

@ -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)
}

10
internal/clip/point.go Normal file
View File

@ -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)
}

39
internal/clip/polygon.go Normal file
View File

@ -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
}

17
internal/clip/rect.go Normal file
View File

@ -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)
}

View File

@ -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
}

View File

@ -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!")
}
}
}

View File

@ -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
}

View File

@ -14,8 +14,8 @@ import (
"github.com/tidwall/buntdb"
"github.com/tidwall/redcon"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server"
)
// AsyncHooks indicates that the hooks should happen in the background.

View File

@ -10,7 +10,7 @@ import (
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/internal/log"
)
var errCorruptedAOF = errors.New("corrupted aof file")

View File

@ -8,10 +8,10 @@ import (
"strings"
"time"
"github.com/tidwall/tile38/pkg/collection"
"github.com/tidwall/tile38/pkg/core"
"github.com/tidwall/tile38/pkg/geojson"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/geojson"
"github.com/tidwall/tile38/internal/log"
)
const maxkeys = 8
@ -127,19 +127,12 @@ func (c *Controller) aofshrink() {
}
}
}
switch obj := obj.(type) {
default:
if obj.IsGeometry() {
values = append(values, "object")
values = append(values, obj.JSON())
} else {
values = append(values, "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))
if objIsSpatial(obj) {
values = append(values, "object")
values = append(values, string(obj.AppendJSON(nil)))
} else {
values = append(values, "string")
values = append(values, obj.String())
}
// append the values to the aof buffer

View File

@ -9,8 +9,8 @@ import (
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/core"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/log"
)
// checksum performs a simple md5 checksum on the aof file

View File

@ -9,7 +9,7 @@ import (
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/server"
)
// Conn represents a simple resp connection.

View File

@ -12,8 +12,8 @@ import (
"github.com/tidwall/gjson"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/glob"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/server"
)
const (
@ -21,6 +21,7 @@ const (
defaultProtectedMode = "yes"
)
// Config keys
const (
FollowHost = "follow_host"
FollowPort = "follow_port"

View File

@ -18,14 +18,14 @@ import (
"github.com/tidwall/btree"
"github.com/tidwall/buntdb"
"github.com/tidwall/geojson"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/collection"
"github.com/tidwall/tile38/pkg/core"
"github.com/tidwall/tile38/pkg/endpoint"
"github.com/tidwall/tile38/pkg/expire"
"github.com/tidwall/tile38/pkg/geojson"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/endpoint"
"github.com/tidwall/tile38/internal/expire"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server"
)
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 {
return ListenAndServeEx(host, port, dir, nil, http)
}
// ListenAndServeEx ...
func ListenAndServeEx(host string, port int, dir string, ln *net.Listener, http bool) error {
if core.AppendFileName == "" {
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.luascripts = c.NewScriptMap()
c.luapool = c.NewPool()
c.luascripts = c.newScriptMap()
c.luapool = c.newPool()
defer c.luapool.Shutdown()
if err := os.MkdirAll(dir, 0700); err != nil {

View File

@ -7,13 +7,14 @@ import (
"strings"
"time"
"github.com/mmcloughlin/geohash"
"github.com/tidwall/btree"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/collection"
"github.com/tidwall/tile38/pkg/geojson"
"github.com/tidwall/tile38/pkg/geojson/geohash"
"github.com/tidwall/tile38/pkg/glob"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/server"
)
type fvt struct {
@ -76,10 +77,13 @@ func (c *Controller) cmdBounds(msg *server.Message) (resp.Value, error) {
}
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 {
buf.WriteString(`,"bounds":`)
buf.WriteString(bbox.ExternalJSON())
buf.WriteString(string(bbox.AppendJSON(nil)))
} else {
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.ArrayValue([]resp.Value{
@ -182,21 +186,25 @@ func (c *Controller) cmdGet(msg *server.Message) (resp.Value, error) {
case "object":
if msg.OutputType == server.JSON {
buf.WriteString(`,"object":`)
buf.WriteString(o.JSON())
buf.WriteString(string(o.AppendJSON(nil)))
} else {
vals = append(vals, resp.StringValue(o.String()))
}
case "point":
point := o.CalculatedPoint()
if msg.OutputType == server.JSON {
buf.WriteString(`,"point":`)
buf.WriteString(point.ExternalJSON())
buf.Write(appendJSONSimplePoint(nil, o))
} 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{
resp.StringValue(strconv.FormatFloat(point.Y, '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 {
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 {
return server.NOMessage, errInvalidArgument(sprecision)
}
p, err := o.Geohash(int(precision))
if err != nil {
return server.NOMessage, err
}
center := o.Center()
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(precision))
if msg.OutputType == server.JSON {
buf.WriteString(`"` + p + `"`)
} else {
vals = append(vals, resp.StringValue(p))
}
case "bounds":
bbox := o.CalculatedBBox()
if msg.OutputType == server.JSON {
buf.WriteString(`,"bounds":`)
buf.WriteString(bbox.ExternalJSON())
buf.Write(appendJSONSimpleBounds(nil, o))
} else {
bbox := o.Rect()
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.ArrayValue([]resp.Value{
resp.FloatValue(bbox.Min.Y),
@ -582,7 +588,7 @@ func (c *Controller) parseSetArgs(vs []resp.Value) (
err = errInvalidNumberOfArguments
return
}
d.obj = geojson.String(str)
d.obj = collection.String(str)
case lcb(typ, "point"):
var slat, slon, sz string
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
@ -595,36 +601,36 @@ func (c *Controller) parseSetArgs(vs []resp.Value) (
}
vs, sz, ok = tokenval(vs)
if !ok || sz == "" {
var sp geojson.SimplePoint
sp.Y, err = strconv.ParseFloat(slat, 64)
var x, y float64
y, err = strconv.ParseFloat(slat, 64)
if err != nil {
err = errInvalidArgument(slat)
return
}
sp.X, err = strconv.ParseFloat(slon, 64)
x, err = strconv.ParseFloat(slon, 64)
if err != nil {
err = errInvalidArgument(slon)
return
}
d.obj = sp
d.obj = geojson.NewPoint(geometry.Point{X: x, Y: y})
} else {
var sp geojson.Point
sp.Coordinates.Y, err = strconv.ParseFloat(slat, 64)
var x, y, z float64
y, err = strconv.ParseFloat(slat, 64)
if err != nil {
err = errInvalidArgument(slat)
return
}
sp.Coordinates.X, err = strconv.ParseFloat(slon, 64)
x, err = strconv.ParseFloat(slon, 64)
if err != nil {
err = errInvalidArgument(slon)
return
}
sp.Coordinates.Z, err = strconv.ParseFloat(sz, 64)
z, err = strconv.ParseFloat(sz, 64)
if err != nil {
err = errInvalidArgument(sz)
return
}
d.obj = sp
d.obj = geojson.NewPointZ(geometry.Point{X: x, Y: y}, z)
}
case lcb(typ, "bounds"):
var sminlat, sminlon, smaxlat, smaxlon string
@ -665,40 +671,25 @@ func (c *Controller) parseSetArgs(vs []resp.Value) (
err = errInvalidArgument(smaxlon)
return
}
g := geojson.Polygon{
Coordinates: [][]geojson.Position{
{
{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
d.obj = geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: minlon, Y: minlat},
Max: geometry.Point{X: maxlon, Y: maxlat},
})
case lcb(typ, "hash"):
var sp geojson.SimplePoint
var shash string
if vs, shash, ok = tokenval(vs); !ok || shash == "" {
err = errInvalidNumberOfArguments
return
}
var lat, lon float64
lat, lon, err = geohash.Decode(shash)
if err != nil {
return
}
sp.X = lon
sp.Y = lat
d.obj = sp
lat, lon := geohash.Decode(shash)
d.obj = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
case lcb(typ, "object"):
var object string
if vs, object, ok = tokenval(vs); !ok || object == "" {
err = errInvalidNumberOfArguments
return
}
d.obj, err = geojson.ObjectJSON(object)
d.obj, err = geojson.Parse(object, nil)
if err != nil {
return
}
@ -832,7 +823,7 @@ func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDeta
var fields []string
var values []float64
var xx bool
var updated_count int
var updateCount int
d, fields, values, xx, err = c.parseFSetArgs(vs)
col := c.getCol(d.key)
@ -841,7 +832,7 @@ func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDeta
return
}
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) {
err = errIDNotFound
return
@ -849,7 +840,7 @@ func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDeta
if ok {
d.command = "fset"
d.timestamp = time.Now()
d.updated = updated_count > 0
d.updated = updateCount > 0
fmap := col.FieldMap()
d.fmap = make(map[string]int)
for key, idx := range fmap {
@ -861,7 +852,7 @@ func (c *Controller) cmdFset(msg *server.Message) (res resp.Value, d commandDeta
case server.JSON:
res = resp.StringValue(`{"ok":true,"elapsed":"` + time.Now().Sub(start).String() + "\"}")
case server.RESP:
res = resp.IntegerValue(updated_count)
res = resp.IntegerValue(updateCount)
}
return
}

View File

@ -10,8 +10,8 @@ import (
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server"
)
// MASSINSERT num_keys num_points [minx miny maxx maxy]

View File

@ -7,7 +7,7 @@ import (
"github.com/tidwall/btree"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/server"
)
type exitem struct {

View File

@ -5,10 +5,11 @@ import (
"strconv"
"time"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/gjson"
"github.com/tidwall/tile38/pkg/geojson"
"github.com/tidwall/tile38/pkg/glob"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/server"
)
// 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
}
func objIsSpatial(obj geojson.Object) bool {
_, ok := obj.(geojson.Spatial)
return ok
}
func hookJSONString(hookName string, metas []FenceMeta) string {
return string(appendHookDetails(nil, hookName, metas))
}
@ -63,7 +70,7 @@ func fenceMatch(
return nil
}
}
if details.obj == nil || !details.obj.IsGeometry() {
if details.obj == nil || !objIsSpatial(details.obj) {
return nil
}
if details.command == "fset" {
@ -120,12 +127,11 @@ func fenceMatch(
// Maybe the old object and new object create a line that crosses the fence.
// Must detect for that possibility.
if details.oldObj != nil {
ls := geojson.LineString{
Coordinates: []geojson.Position{
details.oldObj.CalculatedPoint(),
details.obj.CalculatedPoint(),
},
}
ls := geojson.NewLineString(geometry.NewLine(
[]geometry.Point{
details.oldObj.Center(),
details.obj.Center(),
}, geometry.DefaultIndex))
temp := false
if fence.cmd == "within" {
// because we are testing if the line croses the area we need to use
@ -165,7 +171,7 @@ func fenceMatch(
sw.mu.Lock()
var distance float64
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.fullFields = true
@ -260,7 +266,7 @@ func extendRoamMessage(
nmsg = append(nmsg, `,"id":`...)
nmsg = appendJSONString(nmsg, match.id)
nmsg = append(nmsg, `,"object":`...)
nmsg = append(nmsg, match.obj.JSON()...)
nmsg = match.obj.AppendJSON(nmsg)
nmsg = append(nmsg, `,"meters":`...)
nmsg = strconv.AppendFloat(nmsg,
math.Floor(match.meters*1000)/1000, 'f', -1, 64)
@ -270,9 +276,11 @@ func extendRoamMessage(
if col != nil {
obj, _, ok := col.Get(match.id)
if ok {
nmsg = append(nmsg,
`{"id":`+jsonString(match.id)+
`,"self":true,"object":`+obj.JSON()+`}`...)
nmsg = append(nmsg, `{"id":`...)
nmsg = appendJSONString(nmsg, match.id)
nmsg = append(nmsg, `,"self":true,"object":`...)
nmsg = obj.AppendJSON(nmsg)
nmsg = append(nmsg, '}')
}
pattern := match.id + fence.roam.scan
iterator := func(
@ -282,9 +290,11 @@ func extendRoamMessage(
return true
}
if matched, _ := glob.Match(pattern, oid); matched {
nmsg = append(nmsg,
`,{"id":`+jsonString(oid)+
`,"object":`+o.JSON()+`}`...)
nmsg = append(nmsg, `,{"id":`...)
nmsg = appendJSONString(nmsg, oid)
nmsg = append(nmsg, `,"object":`...)
nmsg = o.AppendJSON(nmsg)
nmsg = append(nmsg, '}')
}
return true
}
@ -323,34 +333,22 @@ func makemsg(
}
func fenceMatchObject(fence *liveFenceSwitches, obj geojson.Object) bool {
if obj == nil {
gobj, _ := obj.(geojson.Object)
if gobj == nil {
return false
}
if fence.roam.on {
// we need to check this object against
return false
}
if fence.cmd == "nearby" {
return obj.Nearby(geojson.Position{X: fence.lon, Y: fence.lat, Z: 0}, fence.meters)
}
if fence.cmd == "within" {
if fence.o != nil {
return obj.Within(fence.o)
}
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},
})
switch fence.cmd {
case "nearby":
// nearby is an INTERSECT on a Circle
return gobj.Intersects(fence.obj)
case "within":
return gobj.Within(fence.obj)
case "intersects":
return gobj.Intersects(fence.obj)
}
return false
}
@ -359,57 +357,58 @@ func fenceMatchRoam(
c *Controller, fence *liveFenceSwitches,
tkey, tid string, obj geojson.Object,
) (nearbys, faraways []roamMatch) {
col := c.getCol(fence.roam.key)
if col == nil {
return
}
p := obj.CalculatedPoint()
prevNearbys := fence.roam.nearbys[tid]
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 {
if c.hasExpired(fence.roam.key, id) {
return true
}
var idMatch bool
if id == tid {
return true // skip self
}
if fence.roam.pattern {
idMatch, _ = glob.Match(fence.roam.id, id)
} else {
idMatch = fence.roam.id == id
}
if !idMatch {
return true
}
if newNearbys == nil {
newNearbys = make(map[string]bool)
}
newNearbys[id] = true
prev := prevNearbys[id]
if prev {
delete(prevNearbys, id)
}
match := roamMatch{
id: id,
obj: obj,
meters: obj.CalculatedPoint().DistanceTo(p),
}
if !prev || !fence.nodwell {
// brand new "nearby"
nearbys = append(nearbys, match)
}
col.Intersects(obj, 0, func(
id string, obj2 geojson.Object, fields []float64,
) bool {
if c.hasExpired(fence.roam.key, id) {
return true
},
)
}
var idMatch bool
if id == tid {
return true // skip self
}
if fence.roam.pattern {
idMatch, _ = glob.Match(fence.roam.id, id)
} else {
idMatch = fence.roam.id == id
}
if !idMatch {
return true
}
if newNearbys == nil {
newNearbys = make(map[string]bool)
}
newNearbys[id] = true
prev := prevNearbys[id]
if prev {
delete(prevNearbys, id)
}
match := roamMatch{
id: id,
obj: obj2,
meters: obj.Distance(obj2),
}
if !prev || !fence.nodwell {
// brand new "nearby"
nearbys = append(nearbys, match)
}
return true
})
for id := range prevNearbys {
obj, _, ok := col.Get(id)
obj2, _, ok := col.Get(id)
if ok && !c.hasExpired(fence.roam.key, id) {
faraways = append(faraways, roamMatch{
id: id, obj: obj,
meters: obj.CalculatedPoint().DistanceTo(p),
id: id,
obj: obj2,
meters: obj.Distance(obj2),
})
}
}

View File

@ -10,9 +10,9 @@ import (
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/core"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server"
)
var errNoLongerFollowing = errors.New("no longer following")

View File

@ -11,10 +11,10 @@ import (
"github.com/tidwall/buntdb"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/endpoint"
"github.com/tidwall/tile38/pkg/glob"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/endpoint"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server"
)
var hookLogSetDefaults = &buntdb.SetOptions{

View File

@ -7,12 +7,12 @@ import (
"strings"
"time"
"github.com/tidwall/geojson"
"github.com/tidwall/gjson"
"github.com/tidwall/resp"
"github.com/tidwall/sjson"
"github.com/tidwall/tile38/pkg/collection"
"github.com/tidwall/tile38/pkg/geojson"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/server"
)
func appendJSONString(b []byte, s string) []byte {
@ -41,6 +41,37 @@ func jsonString(s string) string {
b[len(b)-1] = '"'
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 {
b = append(b, '"')
@ -174,9 +205,7 @@ func (c *Controller) cmdJset(msg *server.Message) (res resp.Value, d commandDeta
var geoobj bool
o, _, ok := col.Get(id)
if ok {
if _, ok := o.(geojson.String); !ok {
geoobj = true
}
geoobj = objIsSpatial(o)
json = o.String()
}
if raw {
@ -208,7 +237,7 @@ func (c *Controller) cmdJset(msg *server.Message) (res resp.Value, d commandDeta
d.key = key
d.id = id
d.obj = geojson.String(json)
d.obj = collection.String(json)
d.timestamp = time.Now()
d.updated = true
@ -248,9 +277,7 @@ func (c *Controller) cmdJdel(msg *server.Message) (res resp.Value, d commandDeta
var geoobj bool
o, _, ok := col.Get(id)
if ok {
if _, ok := o.(geojson.String); !ok {
geoobj = true
}
geoobj = objIsSpatial(o)
json = o.String()
}
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.id = id
d.obj = geojson.String(json)
d.obj = collection.String(json)
d.timestamp = time.Now()
d.updated = true

View File

@ -7,8 +7,8 @@ import (
"github.com/tidwall/btree"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/glob"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/server"
)
func (c *Controller) cmdKeys(msg *server.Message) (res resp.Value, err error) {

View File

@ -8,8 +8,8 @@ import (
"net"
"sync"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server"
)
type liveBuffer struct {

View File

@ -5,7 +5,7 @@ import (
"time"
"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) {

View File

@ -11,8 +11,8 @@ import (
"github.com/tidwall/match"
"github.com/tidwall/redcon"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server"
)
const (

View File

@ -5,8 +5,8 @@ import (
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/log"
"github.com/tidwall/tile38/internal/server"
)
func (c *Controller) cmdReadOnly(msg *server.Message) (res resp.Value, err error) {

View File

@ -6,9 +6,9 @@ import (
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/geojson"
"github.com/tidwall/tile38/pkg/glob"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/geojson"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/server"
)
func (c *Controller) cmdScanArgs(vs []resp.Value) (

View File

@ -7,11 +7,13 @@ import (
"strconv"
"sync"
"github.com/mmcloughlin/geohash"
"github.com/tidwall/geojson"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/collection"
"github.com/tidwall/tile38/pkg/geojson"
"github.com/tidwall/tile38/pkg/glob"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/clip"
"github.com/tidwall/tile38/internal/collection"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/server"
)
const limitItems = 100
@ -58,6 +60,7 @@ type scanWriter struct {
respOut resp.Value
}
// ScanWriterParams ...
type ScanWriterParams struct {
id string
o geojson.Object
@ -65,8 +68,7 @@ type ScanWriterParams struct {
distance float64
noLock bool
ignoreGlobMatch bool
clip bool
clipbox geojson.BBox
clip geojson.Object
}
func (c *Controller) newScanWriter(
@ -199,7 +201,9 @@ func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []fl
for _, where := range sw.wheres {
if where.field == "z" {
if !gotz {
z = o.CalculatedPoint().Z
if point, ok := o.(*geojson.Point); ok {
z = point.Z()
}
}
if !where.match(z) {
return
@ -253,7 +257,9 @@ func (sw *scanWriter) fieldMatch(fields []float64, o geojson.Object) (fvals []fl
for _, where := range sw.wheres {
if where.field == "z" {
if !gotz {
z = o.CalculatedPoint().Z
if point, ok := o.(*geojson.Point); ok {
z = point.Z()
}
}
if !where.match(z) {
return
@ -344,8 +350,8 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool {
if sw.output == outputCount {
return sw.count < sw.limit
}
if opts.clip {
opts.o = opts.o.Clipped(opts.clipbox)
if opts.clip != nil {
opts.o = clip.Clip(opts.o, opts.clip)
}
switch sw.msg.OutputType {
case server.JSON:
@ -392,23 +398,21 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool {
wr.WriteString(`{"id":` + jsonString(opts.id))
switch sw.output {
case outputObjects:
wr.WriteString(`,"object":` + opts.o.JSON())
wr.WriteString(`,"object":` + string(opts.o.AppendJSON(nil)))
case outputPoints:
wr.WriteString(`,"point":` + opts.o.CalculatedPoint().ExternalJSON())
wr.WriteString(`,"point":` + string(appendJSONSimplePoint(nil, opts.o)))
case outputHashes:
p, err := opts.o.Geohash(int(sw.precision))
if err != nil {
p = ""
}
center := opts.o.Center()
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(sw.precision))
wr.WriteString(`,"hash":"` + p + `"`)
case outputBounds:
wr.WriteString(`,"bounds":` + opts.o.CalculatedBBox().ExternalJSON())
wr.WriteString(`,"bounds":` + string(appendJSONSimpleBounds(nil, opts.o)))
}
wr.WriteString(jsfields)
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(`}`)
@ -424,12 +428,16 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool {
case outputObjects:
vals = append(vals, resp.StringValue(opts.o.String()))
case outputPoints:
point := opts.o.CalculatedPoint()
if point.Z != 0 {
point := opts.o.Center()
var z float64
if point, ok := opts.o.(*geojson.Point); ok {
z = point.Z()
}
if z != 0 {
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.FloatValue(point.Y),
resp.FloatValue(point.X),
resp.FloatValue(point.Z),
resp.FloatValue(z),
}))
} else {
vals = append(vals, resp.ArrayValue([]resp.Value{
@ -438,13 +446,11 @@ func (sw *scanWriter) writeObject(opts ScanWriterParams) bool {
}))
}
case outputHashes:
p, err := opts.o.Geohash(int(sw.precision))
if err != nil {
p = ""
}
center := opts.o.Center()
p := geohash.EncodeWithPrecision(center.Y, center.X, uint(sw.precision))
vals = append(vals, resp.StringValue(p))
case outputBounds:
bbox := opts.o.CalculatedBBox()
bbox := opts.o.Rect()
vals = append(vals, resp.ArrayValue([]resp.Value{
resp.ArrayValue([]resp.Value{
resp.FloatValue(bbox.Min.Y),

View File

@ -14,7 +14,7 @@ import (
"time"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/server"
"github.com/yuin/gopher-lua"
luajson "layeh.com/gopher-json"
)
@ -39,8 +39,8 @@ type lStatePool struct {
total int
}
// NewPool returns a new pool of lua states
func (c *Controller) NewPool() *lStatePool {
// newPool returns a new pool of lua states
func (c *Controller) newPool() *lStatePool {
pl := &lStatePool{
saved: make([]*lua.LState, iniLuaPoolSize),
c: c,
@ -206,7 +206,7 @@ func (sm *lScriptMap) Flush() {
}
// NewScriptMap returns a new map with lua scripts
func (c *Controller) NewScriptMap() *lScriptMap {
func (c *Controller) newScriptMap() *lScriptMap {
return &lScriptMap{
scripts: make(map[string]*lua.FunctionProto),
}

View File

@ -8,24 +8,24 @@ import (
"strings"
"time"
"github.com/mmcloughlin/geohash"
"github.com/tidwall/geojson"
"github.com/tidwall/geojson/geometry"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/bing"
"github.com/tidwall/tile38/pkg/geojson"
"github.com/tidwall/tile38/pkg/geojson/geohash"
"github.com/tidwall/tile38/pkg/glob"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/internal/bing"
"github.com/tidwall/tile38/internal/glob"
"github.com/tidwall/tile38/internal/server"
)
const defaultCircleSteps = 64
type liveFenceSwitches struct {
searchScanBaseTokens
lat, lon, meters float64
o geojson.Object
minLat, minLon float64
maxLat, maxLon float64
cmd string
roam roamSwitches
knn bool
groups map[string]string
obj geojson.Object
cmd string
roam roamSwitches
knn bool
groups map[string]string
}
type roamSwitches struct {
@ -102,9 +102,14 @@ func (c *Controller) cmdSearchArgs(
err = errInvalidArgument(typ)
return
}
s.meters = -1 // this will become non-negative if search is within a circle
switch ltyp {
case "point":
fallthrough
case "circle":
if s.clip {
err = errInvalidArgument("cannnot clip with " + ltyp)
return
}
var slat, slon, smeters string
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
err = errInvalidNumberOfArguments
@ -114,11 +119,6 @@ func (c *Controller) cmdSearchArgs(
err = errInvalidNumberOfArguments
return
}
if s.clip {
err = errInvalidArgument("cannnot clip with point")
}
umeters := true
if vs, smeters, ok = tokenval(vs); !ok || smeters == "" {
umeters = false
@ -132,72 +132,42 @@ func (c *Controller) cmdSearchArgs(
return
}
}
if s.lat, err = strconv.ParseFloat(slat, 64); err != nil {
var lat, lon, meters float64
if lat, err = strconv.ParseFloat(slat, 64); err != nil {
err = errInvalidArgument(slat)
return
}
if s.lon, err = strconv.ParseFloat(slon, 64); err != nil {
if lon, err = strconv.ParseFloat(slon, 64); err != nil {
err = errInvalidArgument(slon)
return
}
if umeters {
if s.meters, err = strconv.ParseFloat(smeters, 64); err != nil {
if meters, err = strconv.ParseFloat(smeters, 64); err != nil {
err = errInvalidArgument(smeters)
return
}
if s.meters < 0 {
if meters < 0 {
err = errInvalidArgument(smeters)
return
}
}
case "circle":
if s.clip {
err = errInvalidArgument("cannnot clip with circle")
}
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
if s.knn {
s.obj = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
} else {
s.obj = geojson.NewCircle(geometry.Point{X: lon, Y: lat},
meters, defaultCircleSteps)
}
case "object":
if s.clip {
err = errInvalidArgument("cannnot clip with object")
return
}
var obj string
if vs, obj, ok = tokenval(vs); !ok || obj == "" {
err = errInvalidNumberOfArguments
return
}
s.o, err = geojson.ObjectJSON(obj)
s.obj, err = geojson.Parse(obj, nil)
if err != nil {
return
}
@ -219,42 +189,54 @@ func (c *Controller) cmdSearchArgs(
err = errInvalidNumberOfArguments
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)
return
}
if s.minLon, err = strconv.ParseFloat(sminLon, 64); err != nil {
if minLon, err = strconv.ParseFloat(sminLon, 64); err != nil {
err = errInvalidArgument(sminLon)
return
}
if s.maxLat, err = strconv.ParseFloat(smaxlat, 64); err != nil {
if maxLat, err = strconv.ParseFloat(smaxlat, 64); err != nil {
err = errInvalidArgument(smaxlat)
return
}
if s.maxLon, err = strconv.ParseFloat(smaxlon, 64); err != nil {
if maxLon, err = strconv.ParseFloat(smaxlon, 64); err != nil {
err = errInvalidArgument(smaxlon)
return
}
s.obj = geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: minLon, Y: minLat},
Max: geometry.Point{X: maxLon, Y: maxLat},
})
case "hash":
var hash string
if vs, hash, ok = tokenval(vs); !ok || hash == "" {
err = errInvalidNumberOfArguments
return
}
if s.minLat, s.minLon, s.maxLat, s.maxLon, err = geohash.Bounds(hash); err != nil {
err = errInvalidArgument(hash)
return
}
box := geohash.BoundingBox(hash)
s.obj = geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: box.MinLng, Y: box.MinLat},
Max: geometry.Point{X: box.MaxLng, Y: box.MaxLat},
})
case "quadkey":
var key string
if vs, key, ok = tokenval(vs); !ok || key == "" {
err = errInvalidNumberOfArguments
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)
return
}
s.obj = geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: minLon, Y: minLat},
Max: geometry.Point{X: maxLon, Y: maxLat},
})
case "tile":
var sx, sy, sz string
if vs, sx, ok = tokenval(vs); !ok || sx == "" {
@ -283,7 +265,12 @@ func (c *Controller) cmdSearchArgs(
err = errInvalidArgument(sz)
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":
if s.clip {
err = errInvalidArgument("cannnot clip with get")
@ -302,20 +289,11 @@ func (c *Controller) cmdSearchArgs(
err = errKeyNotFound
return
}
o, _, ok := col.Get(id)
s.obj, _, ok = col.Get(id)
if !ok {
err = errIDNotFound
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":
s.roam.on = true
if vs, s.roam.key, ok = tokenval(vs); !ok || s.roam.key == "" {
@ -336,7 +314,6 @@ func (c *Controller) cmdSearchArgs(
err = errInvalidArgument(smeters)
return
}
var scan string
if vs, scan, ok = tokenval(vs); ok {
if strings.ToLower(scan) != "scan" {
@ -383,7 +360,6 @@ func (c *Controller) cmdNearby(msg *server.Message) (res resp.Value, err error)
if s.fence {
return server.NOMessage, s
}
minZ, maxZ := zMinMaxFromWheres(s.wheres)
sw, err := c.newScanWriter(
wr, msg, s.key, s.output, s.precision, s.glob, false,
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 {
distance = *dist
} else {
distance = o.CalculatedPoint().DistanceTo(geojson.Position{X: s.lon, Y: s.lat, Z: 0})
distance = o.Distance(s.obj)
}
}
return sw.writeObject(ScanWriterParams{
@ -416,16 +392,16 @@ func (c *Controller) cmdNearby(msg *server.Message) (res resp.Value, err error)
})
}
if s.knn {
c.nearestNeighbors(&s, sw, s.lat, s.lon, &matched, iter)
c.nearestNeighbors(&s, sw, s.obj, &matched, iter)
} else {
sw.col.Nearby(s.sparse, s.lat, s.lon, s.meters, minZ, maxZ,
func(id string, o geojson.Object, fields []float64) bool {
if c.hasExpired(s.key, id) {
return true
}
return iter(id, o, fields, nil)
},
)
sw.col.Intersects(s.obj, s.sparse, func(
id string, o geojson.Object, fields []float64,
) bool {
if c.hasExpired(s.key, id) {
return true
}
return iter(id, o, fields, nil)
})
}
}
sw.writeFoot()
@ -443,11 +419,13 @@ type iterItem struct {
dist float64
}
func (c *Controller) nearestNeighbors(s *liveFenceSwitches, sw *scanWriter, lat, lon float64, matched *uint32,
iter func(id string, o geojson.Object, fields []float64, dist *float64) bool) {
func (c *Controller) nearestNeighbors(
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)
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) {
return true
}
@ -458,7 +436,7 @@ func (c *Controller) nearestNeighbors(s *liveFenceSwitches, sw *scanWriter, lat,
if !match {
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})
if !keepGoing {
return false
@ -517,46 +495,40 @@ func (c *Controller) cmdWithinOrIntersects(cmd string, msg *server.Message) (res
}
sw.writeHead()
if sw.col != nil {
minZ, maxZ := zMinMaxFromWheres(s.wheres)
if cmd == "within" {
sw.col.Within(s.sparse,
s.o,
s.minLat, s.minLon, s.maxLat, s.maxLon,
s.lat, s.lon, s.meters,
minZ, maxZ,
func(id string, o geojson.Object, fields []float64) bool {
if c.hasExpired(s.key, id) {
return true
}
return sw.writeObject(ScanWriterParams{
id: id,
o: o,
fields: fields,
noLock: true,
})
},
)
sw.col.Within(s.obj, s.sparse, func(
id string, o geojson.Object, fields []float64,
) bool {
if c.hasExpired(s.key, id) {
return true
}
return sw.writeObject(ScanWriterParams{
id: id,
o: o,
fields: fields,
noLock: true,
})
})
} else if cmd == "intersects" {
sw.col.Intersects(s.sparse,
s.o,
s.minLat, s.minLon, s.maxLat, s.maxLon,
s.lat, s.lon, s.meters,
minZ, maxZ,
s.clip,
func(id string, o geojson.Object, fields []float64, clipbox geojson.BBox) bool {
if c.hasExpired(s.key, id) {
return true
}
return sw.writeObject(ScanWriterParams{
id: id,
o: o,
fields: fields,
noLock: true,
clip: s.clip,
clipbox: clipbox,
})
},
)
sw.col.Intersects(s.obj, s.sparse, func(
id string,
o geojson.Object,
fields []float64,
) bool {
if c.hasExpired(s.key, id) {
return true
}
params := ScanWriterParams{
id: id,
o: o,
fields: fields,
noLock: true,
}
if s.clip {
params.clip = s.obj
}
return sw.writeObject(params)
})
}
}
sw.writeFoot()

View File

@ -12,8 +12,8 @@ import (
"github.com/tidwall/btree"
"github.com/tidwall/resp"
"github.com/tidwall/tile38/pkg/core"
"github.com/tidwall/tile38/pkg/server"
"github.com/tidwall/tile38/core"
"github.com/tidwall/tile38/internal/server"
)
func (c *Controller) cmdStats(msg *server.Message) (res resp.Value, err error) {

View File

@ -6,7 +6,7 @@ import (
"time"
"github.com/garyburd/redigo/redis"
"github.com/tidwall/tile38/pkg/log"
"github.com/tidwall/tile38/internal/log"
)
const (

View File

@ -6,7 +6,7 @@ import (
"sync"
"time"
"github.com/tidwall/tile38/pkg/hservice"
"github.com/tidwall/tile38/internal/hservice"
"golang.org/x/net/context"
"google.golang.org/grpc"
)

Some files were not shown because too many files have changed in this diff Show More