mirror of https://github.com/tidwall/tile38.git
Merge net and circle optz
This commit is contained in:
commit
766e0c941e
|
@ -243,7 +243,7 @@
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
digest = "1:145703130ac1de36086ab350337777161f9c1d791e81a73659ac1f569e15b5e5"
|
digest = "1:3307384a763736cbcfa625076939fe9a240e5f5c9d6ace507fa4fd1f4f6944d6"
|
||||||
name = "github.com/tidwall/geojson"
|
name = "github.com/tidwall/geojson"
|
||||||
packages = [
|
packages = [
|
||||||
".",
|
".",
|
||||||
|
@ -251,7 +251,7 @@
|
||||||
"geometry",
|
"geometry",
|
||||||
]
|
]
|
||||||
pruneopts = ""
|
pruneopts = ""
|
||||||
revision = "dbcb73c57c65ff784ce2ccaad3f062c9787d6f81"
|
revision = "553da6f08f84f544b5482743fe73c3989facc578"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
digest = "1:3ddca2bd5496c6922a2a9e636530e178a43c2a534ea6634211acdc7d10222794"
|
digest = "1:3ddca2bd5496c6922a2a9e636530e178a43c2a534ea6634211acdc7d10222794"
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -8,10 +8,13 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tidwall/redbench"
|
"github.com/tidwall/redbench"
|
||||||
|
"github.com/tidwall/redcon"
|
||||||
|
"github.com/tidwall/tile38/cmd/tile38-benchmark/az"
|
||||||
"github.com/tidwall/tile38/core"
|
"github.com/tidwall/tile38/core"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +27,8 @@ var (
|
||||||
pipeline = 1
|
pipeline = 1
|
||||||
csv = false
|
csv = false
|
||||||
json = false
|
json = false
|
||||||
tests = "PING,SET,GET,INTERSECTS,WITHIN,NEARBY,EVAL"
|
allTests = "PING,SET,GET,INTERSECTS,WITHIN,NEARBY,EVAL"
|
||||||
|
tests = allTests
|
||||||
redis = false
|
redis = false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -179,7 +183,34 @@ func main() {
|
||||||
}
|
}
|
||||||
opts := fillOpts()
|
opts := fillOpts()
|
||||||
addr = fmt.Sprintf("%s:%d", hostname, port)
|
addr = fmt.Sprintf("%s:%d", hostname, port)
|
||||||
|
|
||||||
|
testsArr := strings.Split(allTests, ",")
|
||||||
|
var subtract bool
|
||||||
|
var add bool
|
||||||
for _, test := range strings.Split(tests, ",") {
|
for _, test := range strings.Split(tests, ",") {
|
||||||
|
if strings.HasPrefix(test, "-") {
|
||||||
|
if add {
|
||||||
|
os.Stderr.Write([]byte("test flag cannot mix add and subtract\n"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
subtract = true
|
||||||
|
for i := range testsArr {
|
||||||
|
if strings.ToLower(testsArr[i]) == strings.ToLower(test[1:]) {
|
||||||
|
testsArr = append(testsArr[:i], testsArr[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if subtract {
|
||||||
|
add = true
|
||||||
|
os.Stderr.Write([]byte("test flag cannot mix add and subtract\n"))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !subtract {
|
||||||
|
testsArr = strings.Split(tests, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range testsArr {
|
||||||
switch strings.ToUpper(strings.TrimSpace(test)) {
|
switch strings.ToUpper(strings.TrimSpace(test)) {
|
||||||
case "PING":
|
case "PING":
|
||||||
redbench.Bench("PING", addr, opts, prepFn,
|
redbench.Bench("PING", addr, opts, prepFn,
|
||||||
|
@ -280,8 +311,9 @@ func main() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "INTERSECTS",
|
case "INTERSECTS",
|
||||||
"INTERSECTS-RECT", "INTERSECTS-RECT-1000", "INTERSECTS-RECT-10000", "INTERSECTS-RECT-100000",
|
"INTERSECTS-BOUNDS", "INTERSECTS-BOUNDS-1000", "INTERSECTS-BOUNDS-10000", "INTERSECTS-BOUNDS-100000",
|
||||||
"INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-1000", "INTERSECTS-CIRCLE-10000", "INTERSECTS-CIRCLE-100000":
|
"INTERSECTS-CIRCLE", "INTERSECTS-CIRCLE-1000", "INTERSECTS-CIRCLE-10000", "INTERSECTS-CIRCLE-100000",
|
||||||
|
"INTERSECTS-AZ":
|
||||||
if redis {
|
if redis {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -368,6 +400,51 @@ func main() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(test)) {
|
||||||
|
case "INTERSECTS", "INTERSECTS-AZ":
|
||||||
|
var mu sync.Mutex
|
||||||
|
var loaded bool
|
||||||
|
redbench.Bench("INTERSECTS (intersects-az limit 5)", addr, opts, func(conn net.Conn) bool {
|
||||||
|
func() {
|
||||||
|
mu.Lock()
|
||||||
|
defer mu.Unlock()
|
||||||
|
if loaded {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
p := make([]byte, 0xFF)
|
||||||
|
conn.Write([]byte("GET keys:bench:geo az point\r\n"))
|
||||||
|
n, err := conn.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if string(p[:n]) != "$-1\r\n" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
args := []string{"SET", "key:bench:geo", "az", "object", az.JSON}
|
||||||
|
out := redcon.AppendArray(nil, len(args))
|
||||||
|
for _, arg := range args {
|
||||||
|
out = redcon.AppendBulkString(out, arg)
|
||||||
|
}
|
||||||
|
conn.Write(out)
|
||||||
|
n, err = conn.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if string(p[:n]) != "+OK\r\n" {
|
||||||
|
panic("expected OK")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return prepFn(conn)
|
||||||
|
},
|
||||||
|
func(buf []byte) []byte {
|
||||||
|
args := []string{"INTERSECTS", "key:bench", "LIMIT", "5",
|
||||||
|
"COUNT", "GET", "key:bench:geo", "az"}
|
||||||
|
return redbench.AppendCommand(buf, args...)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
case "WITHIN",
|
case "WITHIN",
|
||||||
"WITHIN-RECT", "WITHIN-RECT-1000", "WITHIN-RECT-10000", "WITHIN-RECT-100000",
|
"WITHIN-RECT", "WITHIN-RECT-1000", "WITHIN-RECT-10000", "WITHIN-RECT-100000",
|
||||||
"WITHIN-CIRCLE", "WITHIN-CIRCLE-1000", "WITHIN-CIRCLE-10000", "WITHIN-CIRCLE-100000":
|
"WITHIN-CIRCLE", "WITHIN-CIRCLE-1000", "WITHIN-CIRCLE-10000", "WITHIN-CIRCLE-100000":
|
||||||
|
@ -544,76 +621,73 @@ func main() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
case "EVAL":
|
case "EVAL":
|
||||||
if redis {
|
if !redis {
|
||||||
break
|
var i int64
|
||||||
}
|
getScript := "return tile38.call('GET', KEYS[1], ARGV[1], 'point')"
|
||||||
var i int64
|
get4Script :=
|
||||||
getScript := "return tile38.call('GET', KEYS[1], ARGV[1], 'point')"
|
"local a = tile38.call('GET', KEYS[1], ARGV[1], 'point');" +
|
||||||
get4Script :=
|
"local b = tile38.call('GET', KEYS[1], ARGV[2], 'point');" +
|
||||||
"local a = tile38.call('GET', KEYS[1], ARGV[1], 'point');" +
|
"local c = tile38.call('GET', KEYS[1], ARGV[3], 'point');" +
|
||||||
"local b = tile38.call('GET', KEYS[1], ARGV[2], 'point');" +
|
"local d = tile38.call('GET', KEYS[1], ARGV[4], 'point');" +
|
||||||
"local c = tile38.call('GET', KEYS[1], ARGV[3], 'point');" +
|
"return d"
|
||||||
"local d = tile38.call('GET', KEYS[1], ARGV[4], 'point');" +
|
|
||||||
"return d"
|
|
||||||
|
|
||||||
setScript := "return tile38.call('SET', KEYS[1], ARGV[1], 'point', ARGV[2], ARGV[3])"
|
setScript := "return tile38.call('SET', KEYS[1], ARGV[1], 'point', ARGV[2], ARGV[3])"
|
||||||
if !opts.Quiet {
|
if !opts.Quiet {
|
||||||
fmt.Println("Scripts to run:")
|
fmt.Println("Scripts to run:")
|
||||||
fmt.Println("GET SCRIPT: " + getScript)
|
fmt.Println("GET SCRIPT: " + getScript)
|
||||||
fmt.Println("GET FOUR SCRIPT: " + get4Script)
|
fmt.Println("GET FOUR SCRIPT: " + get4Script)
|
||||||
fmt.Println("SET SCRIPT: " + setScript)
|
fmt.Println("SET SCRIPT: " + setScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
redbench.Bench("EVAL (set point)", addr, opts, prepFn,
|
||||||
|
func(buf []byte) []byte {
|
||||||
|
i := atomic.AddInt64(&i, 1)
|
||||||
|
lat, lon := randPoint()
|
||||||
|
return redbench.AppendCommand(buf, "EVAL", setScript, "1",
|
||||||
|
"key:bench",
|
||||||
|
"id:"+strconv.FormatInt(i, 10),
|
||||||
|
strconv.FormatFloat(lat, 'f', 5, 64),
|
||||||
|
strconv.FormatFloat(lon, 'f', 5, 64),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
redbench.Bench("EVALNA (set point)", addr, opts, prepFn,
|
||||||
|
func(buf []byte) []byte {
|
||||||
|
i := atomic.AddInt64(&i, 1)
|
||||||
|
lat, lon := randPoint()
|
||||||
|
return redbench.AppendCommand(buf, "EVALNA", setScript, "1",
|
||||||
|
"key:bench",
|
||||||
|
"id:"+strconv.FormatInt(i, 10),
|
||||||
|
strconv.FormatFloat(lat, 'f', 5, 64),
|
||||||
|
strconv.FormatFloat(lon, 'f', 5, 64),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
redbench.Bench("EVALRO (get point)", addr, opts, prepFn,
|
||||||
|
func(buf []byte) []byte {
|
||||||
|
i := atomic.AddInt64(&i, 1)
|
||||||
|
return redbench.AppendCommand(buf, "EVALRO", getScript, "1", "key:bench", "id:"+strconv.FormatInt(i, 10))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
redbench.Bench("EVALRO (get 4 points)", addr, opts, prepFn,
|
||||||
|
func(buf []byte) []byte {
|
||||||
|
i := atomic.AddInt64(&i, 1)
|
||||||
|
return redbench.AppendCommand(buf, "EVALRO", get4Script, "1",
|
||||||
|
"key:bench",
|
||||||
|
"id:"+strconv.FormatInt(i, 10),
|
||||||
|
"id:"+strconv.FormatInt(i+1, 10),
|
||||||
|
"id:"+strconv.FormatInt(i+2, 10),
|
||||||
|
"id:"+strconv.FormatInt(i+3, 10),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
redbench.Bench("EVALNA (get point)", addr, opts, prepFn,
|
||||||
|
func(buf []byte) []byte {
|
||||||
|
i := atomic.AddInt64(&i, 1)
|
||||||
|
return redbench.AppendCommand(buf, "EVALNA", getScript, "1", "key:bench", "id:"+strconv.FormatInt(i, 10))
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
redbench.Bench("EVAL (set point)", addr, opts, prepFn,
|
|
||||||
func(buf []byte) []byte {
|
|
||||||
i := atomic.AddInt64(&i, 1)
|
|
||||||
lat, lon := randPoint()
|
|
||||||
return redbench.AppendCommand(buf, "EVAL", setScript, "1",
|
|
||||||
"key:bench",
|
|
||||||
"id:"+strconv.FormatInt(i, 10),
|
|
||||||
strconv.FormatFloat(lat, 'f', 5, 64),
|
|
||||||
strconv.FormatFloat(lon, 'f', 5, 64),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
redbench.Bench("EVALNA (set point)", addr, opts, prepFn,
|
|
||||||
func(buf []byte) []byte {
|
|
||||||
i := atomic.AddInt64(&i, 1)
|
|
||||||
lat, lon := randPoint()
|
|
||||||
return redbench.AppendCommand(buf, "EVALNA", setScript, "1",
|
|
||||||
"key:bench",
|
|
||||||
"id:"+strconv.FormatInt(i, 10),
|
|
||||||
strconv.FormatFloat(lat, 'f', 5, 64),
|
|
||||||
strconv.FormatFloat(lon, 'f', 5, 64),
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
redbench.Bench("EVALRO (get point)", addr, opts, prepFn,
|
|
||||||
func(buf []byte) []byte {
|
|
||||||
i := atomic.AddInt64(&i, 1)
|
|
||||||
args := []string{"EVALRO", getScript, "1", "key:bench", "id:" + strconv.FormatInt(i, 10)}
|
|
||||||
return redbench.AppendCommand(buf, args...)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
redbench.Bench("EVALRO (get 4 points)", addr, opts, prepFn,
|
|
||||||
func(buf []byte) []byte {
|
|
||||||
i := atomic.AddInt64(&i, 1)
|
|
||||||
args := []string{
|
|
||||||
"EVALRO", get4Script, "1",
|
|
||||||
"key:bench",
|
|
||||||
"id:" + strconv.FormatInt(i, 10),
|
|
||||||
"id:" + strconv.FormatInt(i+1, 10),
|
|
||||||
"id:" + strconv.FormatInt(i+2, 10),
|
|
||||||
"id:" + strconv.FormatInt(i+3, 10),
|
|
||||||
}
|
|
||||||
return redbench.AppendCommand(buf, args...)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
redbench.Bench("EVALNA (get point)", addr, opts, prepFn,
|
|
||||||
func(buf []byte) []byte {
|
|
||||||
i := atomic.AddInt64(&i, 1)
|
|
||||||
return redbench.AppendCommand(buf, "EVALNA", getScript, "1", "key:bench", "id:"+strconv.FormatInt(i, 10))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,6 +80,7 @@ Advanced Options:
|
||||||
--http-transport yes/no : HTTP transport (default: yes)
|
--http-transport yes/no : HTTP transport (default: yes)
|
||||||
--protected-mode yes/no : protected mode (default: yes)
|
--protected-mode yes/no : protected mode (default: yes)
|
||||||
--threads num : number of network threads (default: num cores)
|
--threads num : number of network threads (default: num cores)
|
||||||
|
--evio yes/no : use the evio package (default: no)
|
||||||
|
|
||||||
Developer Options:
|
Developer Options:
|
||||||
--dev : enable developer mode
|
--dev : enable developer mode
|
||||||
|
@ -146,10 +147,10 @@ Developer Options:
|
||||||
if i < len(os.Args) {
|
if i < len(os.Args) {
|
||||||
switch strings.ToLower(os.Args[i]) {
|
switch strings.ToLower(os.Args[i]) {
|
||||||
case "no":
|
case "no":
|
||||||
core.ProtectedMode = "no"
|
core.ProtectedMode = false
|
||||||
continue
|
continue
|
||||||
case "yes":
|
case "yes":
|
||||||
core.ProtectedMode = "yes"
|
core.ProtectedMode = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,10 +164,10 @@ Developer Options:
|
||||||
if i < len(os.Args) {
|
if i < len(os.Args) {
|
||||||
switch strings.ToLower(os.Args[i]) {
|
switch strings.ToLower(os.Args[i]) {
|
||||||
case "no":
|
case "no":
|
||||||
core.AppendOnly = "no"
|
core.AppendOnly = false
|
||||||
continue
|
continue
|
||||||
case "yes":
|
case "yes":
|
||||||
core.AppendOnly = "yes"
|
core.AppendOnly = true
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,6 +214,20 @@ Developer Options:
|
||||||
}
|
}
|
||||||
fmt.Fprintf(os.Stderr, "http-transport must be 'yes' or 'no'\n")
|
fmt.Fprintf(os.Stderr, "http-transport must be 'yes' or 'no'\n")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
case "--evio", "-evio":
|
||||||
|
i++
|
||||||
|
if i < len(os.Args) {
|
||||||
|
switch strings.ToLower(os.Args[i]) {
|
||||||
|
case "no":
|
||||||
|
core.Evio = false
|
||||||
|
continue
|
||||||
|
case "yes":
|
||||||
|
core.Evio = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(os.Stderr, "evio must be 'yes' or 'no'\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
nargs = append(nargs, os.Args[i])
|
nargs = append(nargs, os.Args[i])
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,10 @@ var DevMode = false
|
||||||
var ShowDebugMessages = false
|
var ShowDebugMessages = false
|
||||||
|
|
||||||
// ProtectedMode forces Tile38 to default in protected mode.
|
// ProtectedMode forces Tile38 to default in protected mode.
|
||||||
var ProtectedMode = "yes"
|
var ProtectedMode = true
|
||||||
|
|
||||||
// AppendOnly allows for disabling the appendonly file.
|
// AppendOnly allows for disabling the appendonly file.
|
||||||
var AppendOnly = "yes"
|
var AppendOnly = true
|
||||||
|
|
||||||
// AppendFileName allows for custom appendonly file path
|
// AppendFileName allows for custom appendonly file path
|
||||||
var AppendFileName string
|
var AppendFileName string
|
||||||
|
@ -20,3 +20,6 @@ var QueueFileName string
|
||||||
|
|
||||||
// NumThreads is the number of network threads to use.
|
// NumThreads is the number of network threads to use.
|
||||||
var NumThreads int
|
var NumThreads int
|
||||||
|
|
||||||
|
// Evio set the networking to use the evio package.
|
||||||
|
var Evio = false
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package core
|
package core
|
||||||
|
|
||||||
|
// Build variables
|
||||||
var (
|
var (
|
||||||
Version = "0.0.0" // Placeholder for the version
|
Version = "0.0.0" // Placeholder for the version
|
||||||
BuildTime = "" // Placeholder for the build time
|
BuildTime = "" // Placeholder for the build time
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"github.com/tidwall/boxtree/d2"
|
"github.com/tidwall/boxtree/d2"
|
||||||
"github.com/tidwall/btree"
|
"github.com/tidwall/btree"
|
||||||
"github.com/tidwall/geojson"
|
"github.com/tidwall/geojson"
|
||||||
|
"github.com/tidwall/geojson/geo"
|
||||||
"github.com/tidwall/geojson/geometry"
|
"github.com/tidwall/geojson/geometry"
|
||||||
"github.com/tidwall/tile38/internal/ds"
|
"github.com/tidwall/tile38/internal/ds"
|
||||||
)
|
)
|
||||||
|
@ -673,6 +674,30 @@ func (c *Collection) Nearby(
|
||||||
cursor Cursor,
|
cursor Cursor,
|
||||||
iter func(id string, obj geojson.Object, fields []float64) bool,
|
iter func(id string, obj geojson.Object, fields []float64) bool,
|
||||||
) bool {
|
) bool {
|
||||||
|
// First look to see if there's at least one candidate in the circle's
|
||||||
|
// outer rectangle. This is a fast-fail operation.
|
||||||
|
if circle, ok := target.(*geojson.Circle); ok {
|
||||||
|
meters := circle.Meters()
|
||||||
|
if meters > 0 {
|
||||||
|
center := circle.Center()
|
||||||
|
minLat, minLon, maxLat, maxLon :=
|
||||||
|
geo.RectFromCenter(center.Y, center.X, meters)
|
||||||
|
var exists bool
|
||||||
|
c.index.Search(
|
||||||
|
[]float64{minLon, minLat},
|
||||||
|
[]float64{maxLon, maxLat},
|
||||||
|
func(_, _ []float64, itemv interface{}) bool {
|
||||||
|
exists = true
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if !exists {
|
||||||
|
// no candidates
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// do the kNN operation
|
||||||
alive := true
|
alive := true
|
||||||
center := target.Center()
|
center := target.Center()
|
||||||
var count uint64
|
var count uint64
|
||||||
|
|
|
@ -236,7 +236,7 @@ func Serve(host string, port int, dir string, http bool) error {
|
||||||
if err := server.migrateAOF(); err != nil {
|
if err := server.migrateAOF(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if core.AppendOnly == "yes" {
|
if core.AppendOnly == true {
|
||||||
f, err := os.OpenFile(core.AppendFileName, os.O_CREATE|os.O_RDWR, 0600)
|
f, err := os.OpenFile(core.AppendFileName, os.O_CREATE|os.O_RDWR, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -271,11 +271,14 @@ func Serve(host string, port int, dir string, http bool) error {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Start the network server
|
// Start the network server
|
||||||
return server.evioServe()
|
if core.Evio {
|
||||||
|
return server.evioServe()
|
||||||
|
}
|
||||||
|
return server.netServe()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (server *Server) isProtected() bool {
|
func (server *Server) isProtected() bool {
|
||||||
if core.ProtectedMode == "no" {
|
if core.ProtectedMode == false {
|
||||||
// --protected-mode no
|
// --protected-mode no
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -485,6 +488,192 @@ func (server *Server) evioServe() error {
|
||||||
return evio.Serve(events, fmt.Sprintf("%s:%d", server.host, server.port))
|
return evio.Serve(events, fmt.Sprintf("%s:%d", server.host, server.port))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (server *Server) netServe() error {
|
||||||
|
ln, err := net.Listen("tcp", fmt.Sprintf("%s:%d", server.host, server.port))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
log.Infof("Ready to accept connections at %s", ln.Addr())
|
||||||
|
var clientID int64
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func(conn net.Conn) {
|
||||||
|
// open connection
|
||||||
|
// create the client
|
||||||
|
client := new(Client)
|
||||||
|
client.id = int(atomic.AddInt64(&clientID, 1))
|
||||||
|
client.opened = time.Now()
|
||||||
|
client.remoteAddr = conn.RemoteAddr().String()
|
||||||
|
|
||||||
|
// add client to server map
|
||||||
|
server.connsmu.Lock()
|
||||||
|
server.conns[client.id] = client
|
||||||
|
server.connsmu.Unlock()
|
||||||
|
server.statsTotalConns.add(1)
|
||||||
|
|
||||||
|
// set the client keep-alive, if needed
|
||||||
|
if server.config.keepAlive() > 0 {
|
||||||
|
if conn, ok := conn.(*net.TCPConn); ok {
|
||||||
|
conn.SetKeepAlive(true)
|
||||||
|
conn.SetKeepAlivePeriod(
|
||||||
|
time.Duration(server.config.keepAlive()) * time.Second,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Debugf("Opened connection: %s", client.remoteAddr)
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// close connection
|
||||||
|
// delete from server map
|
||||||
|
server.connsmu.Lock()
|
||||||
|
delete(server.conns, client.id)
|
||||||
|
server.connsmu.Unlock()
|
||||||
|
log.Debugf("Closed connection: %s", client.remoteAddr)
|
||||||
|
conn.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
// check if the connection is protected
|
||||||
|
if !strings.HasPrefix(client.remoteAddr, "127.0.0.1:") &&
|
||||||
|
!strings.HasPrefix(client.remoteAddr, "[::1]:") {
|
||||||
|
if server.isProtected() {
|
||||||
|
// This is a protected server. Only loopback is allowed.
|
||||||
|
conn.Write(deniedMessage)
|
||||||
|
return // close connection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
packet := make([]byte, 0xFFFF)
|
||||||
|
for {
|
||||||
|
var close bool
|
||||||
|
n, err := conn.Read(packet)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
in := packet[:n]
|
||||||
|
|
||||||
|
// read the payload packet from the client input stream.
|
||||||
|
packet := client.in.Begin(in)
|
||||||
|
|
||||||
|
// load the pipeline reader
|
||||||
|
pr := &client.pr
|
||||||
|
rdbuf := bytes.NewBuffer(packet)
|
||||||
|
pr.rd = rdbuf
|
||||||
|
pr.wr = client
|
||||||
|
|
||||||
|
msgs, err := pr.ReadMessages()
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
return // close connection
|
||||||
|
}
|
||||||
|
for _, msg := range msgs {
|
||||||
|
// Just closing connection if we have deprecated HTTP or WS connection,
|
||||||
|
// And --http-transport = false
|
||||||
|
if !server.http && (msg.ConnType == WebSocket ||
|
||||||
|
msg.ConnType == HTTP) {
|
||||||
|
close = true // close connection
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if msg != nil && msg.Command() != "" {
|
||||||
|
if client.outputType != Null {
|
||||||
|
msg.OutputType = client.outputType
|
||||||
|
}
|
||||||
|
if msg.Command() == "quit" {
|
||||||
|
if msg.OutputType == RESP {
|
||||||
|
io.WriteString(client, "+OK\r\n")
|
||||||
|
}
|
||||||
|
close = true // close connection
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment last used
|
||||||
|
client.mu.Lock()
|
||||||
|
client.last = time.Now()
|
||||||
|
client.mu.Unlock()
|
||||||
|
|
||||||
|
// update total command count
|
||||||
|
server.statsTotalCommands.add(1)
|
||||||
|
|
||||||
|
// handle the command
|
||||||
|
err := server.handleInputCommand(client, msg)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == goingLive {
|
||||||
|
client.goLiveErr = err
|
||||||
|
client.goLiveMsg = msg
|
||||||
|
// detach
|
||||||
|
var rwc io.ReadWriteCloser = conn
|
||||||
|
client.conn = rwc
|
||||||
|
if len(client.out) > 0 {
|
||||||
|
client.conn.Write(client.out)
|
||||||
|
client.out = nil
|
||||||
|
}
|
||||||
|
client.in = evio.InputStream{}
|
||||||
|
client.pr.rd = rwc
|
||||||
|
client.pr.wr = rwc
|
||||||
|
log.Debugf("Detached connection: %s", client.remoteAddr)
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := server.goLive(
|
||||||
|
client.goLiveErr,
|
||||||
|
&liveConn{conn.RemoteAddr(), rwc},
|
||||||
|
&client.pr,
|
||||||
|
client.goLiveMsg,
|
||||||
|
client.goLiveMsg.ConnType == WebSocket,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
wg.Wait()
|
||||||
|
return // close connection
|
||||||
|
}
|
||||||
|
log.Error(err)
|
||||||
|
return // close connection, NOW
|
||||||
|
}
|
||||||
|
|
||||||
|
client.outputType = msg.OutputType
|
||||||
|
} else {
|
||||||
|
client.Write([]byte("HTTP/1.1 500 Bad Request\r\nConnection: close\r\n\r\n"))
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if msg.ConnType == HTTP || msg.ConnType == WebSocket {
|
||||||
|
close = true // close connection
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
packet = packet[len(packet)-rdbuf.Len():]
|
||||||
|
client.in.End(packet)
|
||||||
|
|
||||||
|
// write to client
|
||||||
|
if len(client.out) > 0 {
|
||||||
|
if atomic.LoadInt32(&server.aofdirty) != 0 {
|
||||||
|
func() {
|
||||||
|
// prewrite
|
||||||
|
server.mu.Lock()
|
||||||
|
defer server.mu.Unlock()
|
||||||
|
server.flushAOF()
|
||||||
|
}()
|
||||||
|
atomic.StoreInt32(&server.aofdirty, 0)
|
||||||
|
}
|
||||||
|
conn.Write(client.out)
|
||||||
|
client.out = nil
|
||||||
|
|
||||||
|
}
|
||||||
|
if close {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type liveConn struct {
|
type liveConn struct {
|
||||||
remoteAddr net.Addr
|
remoteAddr net.Addr
|
||||||
rwc io.ReadWriteCloser
|
rwc io.ReadWriteCloser
|
||||||
|
@ -672,6 +861,7 @@ func (server *Server) handleInputCommand(client *Client, msg *Message) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping. Just send back the response. No need to put through the pipeline.
|
// Ping. Just send back the response. No need to put through the pipeline.
|
||||||
if msg.Command() == "ping" || msg.Command() == "echo" {
|
if msg.Command() == "ping" || msg.Command() == "echo" {
|
||||||
switch msg.OutputType {
|
switch msg.OutputType {
|
||||||
|
@ -689,6 +879,7 @@ func (server *Server) handleInputCommand(client *Client, msg *Message) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
writeErr := func(errMsg string) error {
|
writeErr := func(errMsg string) error {
|
||||||
switch msg.OutputType {
|
switch msg.OutputType {
|
||||||
case JSON:
|
case JSON:
|
||||||
|
@ -732,6 +923,7 @@ func (server *Server) handleInputCommand(client *Client, msg *Message) error {
|
||||||
return writeErr("invalid password")
|
return writeErr("invalid password")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// choose the locking strategy
|
// choose the locking strategy
|
||||||
switch msg.Command() {
|
switch msg.Command() {
|
||||||
default:
|
default:
|
||||||
|
@ -765,6 +957,7 @@ func (server *Server) handleInputCommand(client *Client, msg *Message) error {
|
||||||
"chans", "search", "ttl", "bounds", "server", "info", "type", "jget",
|
"chans", "search", "ttl", "bounds", "server", "info", "type", "jget",
|
||||||
"evalro", "evalrosha":
|
"evalro", "evalrosha":
|
||||||
// read operations
|
// read operations
|
||||||
|
|
||||||
server.mu.RLock()
|
server.mu.RLock()
|
||||||
defer server.mu.RUnlock()
|
defer server.mu.RUnlock()
|
||||||
if server.config.followHost() != "" && !server.fcuponce {
|
if server.config.followHost() != "" && !server.fcuponce {
|
||||||
|
@ -803,7 +996,6 @@ func (server *Server) handleInputCommand(client *Client, msg *Message) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
res, d, err := server.command(msg, client)
|
res, d, err := server.command(msg, client)
|
||||||
|
|
||||||
if res.Type() == resp.Error {
|
if res.Type() == resp.Error {
|
||||||
return writeErr(res.String())
|
return writeErr(res.String())
|
||||||
}
|
}
|
||||||
|
@ -822,7 +1014,6 @@ func (server *Server) handleInputCommand(client *Client, msg *Message) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isRespValueEmptyString(res) {
|
if !isRespValueEmptyString(res) {
|
||||||
var resStr string
|
var resStr string
|
||||||
resStr, err := serializeOutput(res)
|
resStr, err := serializeOutput(res)
|
||||||
|
@ -1094,6 +1285,7 @@ const (
|
||||||
|
|
||||||
// Message is a resp message
|
// Message is a resp message
|
||||||
type Message struct {
|
type Message struct {
|
||||||
|
_command string
|
||||||
Args []string
|
Args []string
|
||||||
ConnType Type
|
ConnType Type
|
||||||
OutputType Type
|
OutputType Type
|
||||||
|
@ -1102,7 +1294,10 @@ type Message struct {
|
||||||
|
|
||||||
// Command returns the first argument as a lowercase string
|
// Command returns the first argument as a lowercase string
|
||||||
func (msg *Message) Command() string {
|
func (msg *Message) Command() string {
|
||||||
return strings.ToLower(msg.Args[0])
|
if msg._command == "" {
|
||||||
|
msg._command = strings.ToLower(msg.Args[0])
|
||||||
|
}
|
||||||
|
return msg._command
|
||||||
}
|
}
|
||||||
|
|
||||||
// PipelineReader ...
|
// PipelineReader ...
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package geojson
|
package geojson
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"math"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/tidwall/geojson/geo"
|
"github.com/tidwall/geojson/geo"
|
||||||
|
@ -9,7 +10,7 @@ import (
|
||||||
|
|
||||||
// Circle ...
|
// Circle ...
|
||||||
type Circle struct {
|
type Circle struct {
|
||||||
Object
|
object Object
|
||||||
center geometry.Point
|
center geometry.Point
|
||||||
meters float64
|
meters float64
|
||||||
haversine float64
|
haversine float64
|
||||||
|
@ -27,25 +28,8 @@ func NewCircle(center geometry.Point, meters float64, steps int) *Circle {
|
||||||
g.center = center
|
g.center = center
|
||||||
g.meters = meters
|
g.meters = meters
|
||||||
g.steps = steps
|
g.steps = steps
|
||||||
if meters <= 0 {
|
if meters > 0 {
|
||||||
g.Object = NewPoint(center)
|
|
||||||
} else {
|
|
||||||
meters = geo.NormalizeDistance(meters)
|
meters = geo.NormalizeDistance(meters)
|
||||||
var points []geometry.Point
|
|
||||||
step := 360.0 / float64(steps)
|
|
||||||
i := 0
|
|
||||||
for deg := 360.0; deg > 0; deg -= step {
|
|
||||||
lat, lon := geo.DestinationPoint(center.Y, center.X, meters, deg)
|
|
||||||
points = append(points, geometry.Point{X: lon, Y: lat})
|
|
||||||
i++
|
|
||||||
}
|
|
||||||
// TODO: account for the pole and antimerdian. In most cases only a
|
|
||||||
// polygon is needed, but when the circle bounds passes the 90/180
|
|
||||||
// lines, we need to create a multipolygon
|
|
||||||
points = append(points, points[0])
|
|
||||||
g.Object = NewPolygon(
|
|
||||||
geometry.NewPoly(points, nil, geometry.DefaultIndexOptions),
|
|
||||||
)
|
|
||||||
g.haversine = geo.DistanceToHaversine(meters)
|
g.haversine = geo.DistanceToHaversine(meters)
|
||||||
}
|
}
|
||||||
return g
|
return g
|
||||||
|
@ -128,7 +112,7 @@ func (g *Circle) Contains(obj Object) bool {
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
// No simple cases, so using polygon approximation.
|
// No simple cases, so using polygon approximation.
|
||||||
return g.Object.Contains(other)
|
return g.getObject().Contains(other)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +169,81 @@ func (g *Circle) Intersects(obj Object) bool {
|
||||||
return false
|
return false
|
||||||
default:
|
default:
|
||||||
// No simple cases, so using polygon approximation.
|
// No simple cases, so using polygon approximation.
|
||||||
return g.Object.Intersects(obj)
|
return g.getObject().Intersects(obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty ...
|
||||||
|
func (g *Circle) Empty() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ForEach ...
|
||||||
|
func (g *Circle) ForEach(iter func(geom Object) bool) bool {
|
||||||
|
return iter(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumPoints ...
|
||||||
|
func (g *Circle) NumPoints() int {
|
||||||
|
// should this be g.steps?
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance ...
|
||||||
|
func (g *Circle) Distance(other Object) float64 {
|
||||||
|
return g.getObject().Distance(other)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rect ...
|
||||||
|
func (g *Circle) Rect() geometry.Rect {
|
||||||
|
return g.getObject().Rect()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spatial ...
|
||||||
|
func (g *Circle) Spatial() Spatial {
|
||||||
|
return g.getObject().Spatial()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Circle) getObject() Object {
|
||||||
|
if g.object != nil {
|
||||||
|
return g.object
|
||||||
|
}
|
||||||
|
return makeCircleObject(g.center, g.meters, g.steps)
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCircleObject(center geometry.Point, meters float64, steps int) Object {
|
||||||
|
if meters <= 0 {
|
||||||
|
return NewPoint(center)
|
||||||
|
}
|
||||||
|
meters = geo.NormalizeDistance(meters)
|
||||||
|
points := make([]geometry.Point, 0, steps+1)
|
||||||
|
|
||||||
|
// calc the four corners
|
||||||
|
maxY, _ := geo.DestinationPoint(center.Y, center.X, meters, 0)
|
||||||
|
_, maxX := geo.DestinationPoint(center.Y, center.X, meters, 90)
|
||||||
|
minY, _ := geo.DestinationPoint(center.Y, center.X, meters, 180)
|
||||||
|
_, minX := geo.DestinationPoint(center.Y, center.X, meters, 270)
|
||||||
|
|
||||||
|
// TODO: detect of pole and antimeridian crossing and generate a
|
||||||
|
// valid multigeometry
|
||||||
|
|
||||||
|
// use the half width of the lat and lon
|
||||||
|
lons := (maxX - minX) / 2
|
||||||
|
lats := (maxY - minY) / 2
|
||||||
|
|
||||||
|
// generate the
|
||||||
|
for th := 0.0; th <= 360.0; th += 360.0 / float64(steps) {
|
||||||
|
radians := (math.Pi / 180) * th
|
||||||
|
x := center.X + lats*math.Cos(radians)
|
||||||
|
y := center.Y + lons*math.Sin(radians)
|
||||||
|
points = append(points, geometry.Point{X: x, Y: y})
|
||||||
|
}
|
||||||
|
// add last connecting point, make a total of steps+1
|
||||||
|
points = append(points, points[0])
|
||||||
|
|
||||||
|
return NewPolygon(
|
||||||
|
geometry.NewPoly(points, nil, &geometry.IndexOptions{
|
||||||
|
Kind: geometry.None,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ func DistanceToHaversine(meters float64) float64 {
|
||||||
return sin * sin
|
return sin * sin
|
||||||
}
|
}
|
||||||
|
|
||||||
// DistanceFromHaversine...
|
// DistanceFromHaversine ...
|
||||||
func DistanceFromHaversine(haversine float64) float64 {
|
func DistanceFromHaversine(haversine float64) float64 {
|
||||||
return earthRadius * 2 * math.Asin(math.Sqrt(haversine))
|
return earthRadius * 2 * math.Asin(math.Sqrt(haversine))
|
||||||
}
|
}
|
||||||
|
@ -88,3 +88,84 @@ func BearingTo(latA, lonA, latB, lonB float64) float64 {
|
||||||
|
|
||||||
return math.Mod(θ*degrees+360, 360)
|
return math.Mod(θ*degrees+360, 360)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RectFromCenter calculates the bounding box surrounding a circle.
|
||||||
|
func RectFromCenter(lat, lon, meters float64) (
|
||||||
|
minLat, minLon, maxLat, maxLon float64,
|
||||||
|
) {
|
||||||
|
|
||||||
|
// see http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates#Latitude
|
||||||
|
lat *= radians
|
||||||
|
lon *= radians
|
||||||
|
|
||||||
|
r := meters / earthRadius // angular radius
|
||||||
|
|
||||||
|
minLat = lat - r
|
||||||
|
maxLat = lat + r
|
||||||
|
|
||||||
|
latT := math.Asin(math.Sin(lat) / math.Cos(r))
|
||||||
|
lonΔ := math.Acos((math.Cos(r) - math.Sin(latT)*math.Sin(lat)) / (math.Cos(latT) * math.Cos(lat)))
|
||||||
|
|
||||||
|
minLon = lon - lonΔ
|
||||||
|
maxLon = lon + lonΔ
|
||||||
|
|
||||||
|
// Adjust for north poll
|
||||||
|
if maxLat > math.Pi/2 {
|
||||||
|
minLon = -math.Pi
|
||||||
|
maxLat = math.Pi / 2
|
||||||
|
maxLon = math.Pi
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust for south poll
|
||||||
|
if minLat < -math.Pi/2 {
|
||||||
|
minLat = -math.Pi / 2
|
||||||
|
minLon = -math.Pi
|
||||||
|
maxLon = math.Pi
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust for wraparound. Remove this if the commented-out condition below this block is added.
|
||||||
|
if minLon < -math.Pi || maxLon > math.Pi {
|
||||||
|
minLon = -math.Pi
|
||||||
|
maxLon = math.Pi
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// Consider splitting area into two bboxes, using the below checks, and erasing above block for performance. See http://janmatuschek.de/LatitudeLongitudeBoundingCoordinates#PolesAnd180thMeridian
|
||||||
|
// Adjust for wraparound if minimum longitude is less than -180 degrees.
|
||||||
|
if lonMin < -math.Pi {
|
||||||
|
// box 1:
|
||||||
|
latMin = latMin
|
||||||
|
latMax = latMax
|
||||||
|
lonMin += 2*math.Pi
|
||||||
|
lonMax = math.Pi
|
||||||
|
// box 2:
|
||||||
|
latMin = latMin
|
||||||
|
latMax = latMax
|
||||||
|
lonMin = -math.Pi
|
||||||
|
lonMax = lonMax
|
||||||
|
}
|
||||||
|
// Adjust for wraparound if maximum longitude is greater than 180 degrees.
|
||||||
|
if lonMax > math.Pi {
|
||||||
|
// box 1:
|
||||||
|
latMin = latMin
|
||||||
|
latMax = latMax
|
||||||
|
lonMin = lonMin
|
||||||
|
lonMax = -math.Pi
|
||||||
|
// box 2:
|
||||||
|
latMin = latMin
|
||||||
|
latMax = latMax
|
||||||
|
lonMin = -math.Pi
|
||||||
|
lonMax -= 2*math.Pi
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
minLon = math.Mod(minLon+3*math.Pi, 2*math.Pi) - math.Pi // normalise to -180..+180°
|
||||||
|
maxLon = math.Mod(maxLon+3*math.Pi, 2*math.Pi) - math.Pi
|
||||||
|
|
||||||
|
minLat *= degrees
|
||||||
|
minLon *= degrees
|
||||||
|
maxLat *= degrees
|
||||||
|
maxLon *= degrees
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package geojson
|
package geojson
|
||||||
|
|
||||||
import "github.com/tidwall/geojson/geometry"
|
import (
|
||||||
|
"github.com/tidwall/geojson/geometry"
|
||||||
|
)
|
||||||
|
|
||||||
// Rect ...
|
// Rect ...
|
||||||
type Rect struct {
|
type Rect struct {
|
||||||
|
|
Loading…
Reference in New Issue