2019-02-09 00:57:29 +03:00
|
|
|
package server
|
|
|
|
|
2019-02-12 15:33:20 +03:00
|
|
|
// TEST command: spatial tests without walking the tree.
|
|
|
|
|
2019-02-09 00:57:29 +03:00
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"fmt"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/mmcloughlin/geohash"
|
|
|
|
"github.com/tidwall/geojson"
|
|
|
|
"github.com/tidwall/geojson/geometry"
|
|
|
|
"github.com/tidwall/resp"
|
|
|
|
"github.com/tidwall/tile38/internal/bing"
|
|
|
|
"github.com/tidwall/tile38/internal/clip"
|
|
|
|
)
|
|
|
|
|
2019-02-12 15:33:20 +03:00
|
|
|
func (s *Server) parseArea(ovs []string, doClip bool) (vs []string, o geojson.Object, err error) {
|
2019-02-09 00:57:29 +03:00
|
|
|
var ok bool
|
|
|
|
var typ string
|
|
|
|
vs = ovs[:]
|
|
|
|
if vs, typ, ok = tokenval(vs); !ok || typ == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
ltyp := strings.ToLower(typ)
|
|
|
|
switch ltyp {
|
|
|
|
case "point":
|
|
|
|
var slat, slon string
|
|
|
|
if vs, slat, ok = tokenval(vs); !ok || slat == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if vs, slon, ok = tokenval(vs); !ok || slon == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var lat, lon float64
|
|
|
|
if lat, err = strconv.ParseFloat(slat, 64); err != nil {
|
|
|
|
err = errInvalidArgument(slat)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if lon, err = strconv.ParseFloat(slon, 64); err != nil {
|
|
|
|
err = errInvalidArgument(slon)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
o = geojson.NewPoint(geometry.Point{X: lon, Y: lat})
|
|
|
|
case "circle":
|
|
|
|
if doClip {
|
2019-02-12 15:33:20 +03:00
|
|
|
err = fmt.Errorf("invalid clip type '%s'", typ)
|
2019-02-09 00:57:29 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
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
|
|
|
|
}
|
|
|
|
var lat, lon, meters float64
|
|
|
|
if lat, err = strconv.ParseFloat(slat, 64); err != nil {
|
|
|
|
err = errInvalidArgument(slat)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if lon, err = strconv.ParseFloat(slon, 64); err != nil {
|
|
|
|
err = errInvalidArgument(slon)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if vs, smeters, ok = tokenval(vs); !ok || smeters == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if meters, err = strconv.ParseFloat(smeters, 64); err != nil {
|
|
|
|
err = errInvalidArgument(smeters)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if meters < 0 {
|
|
|
|
err = errInvalidArgument(smeters)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
o = geojson.NewCircle(geometry.Point{X: lon, Y: lat}, meters, defaultCircleSteps)
|
|
|
|
case "object":
|
|
|
|
if doClip {
|
2019-02-12 15:33:20 +03:00
|
|
|
err = fmt.Errorf("invalid clip type '%s'", typ)
|
2019-02-09 00:57:29 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
var obj string
|
|
|
|
if vs, obj, ok = tokenval(vs); !ok || obj == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
o, err = geojson.Parse(obj, &s.geomParseOpts)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
case "bounds":
|
|
|
|
var sminLat, sminLon, smaxlat, smaxlon string
|
|
|
|
if vs, sminLat, ok = tokenval(vs); !ok || sminLat == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if vs, sminLon, ok = tokenval(vs); !ok || sminLon == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if vs, smaxlat, ok = tokenval(vs); !ok || smaxlat == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if vs, smaxlon, ok = tokenval(vs); !ok || smaxlon == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var minLat, minLon, maxLat, maxLon float64
|
|
|
|
if minLat, err = strconv.ParseFloat(sminLat, 64); err != nil {
|
|
|
|
err = errInvalidArgument(sminLat)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if minLon, err = strconv.ParseFloat(sminLon, 64); err != nil {
|
|
|
|
err = errInvalidArgument(sminLon)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if maxLat, err = strconv.ParseFloat(smaxlat, 64); err != nil {
|
|
|
|
err = errInvalidArgument(smaxlat)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if maxLon, err = strconv.ParseFloat(smaxlon, 64); err != nil {
|
|
|
|
err = errInvalidArgument(smaxlon)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
o = 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
|
|
|
|
}
|
|
|
|
box := geohash.BoundingBox(hash)
|
|
|
|
o = 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
|
|
|
|
}
|
|
|
|
var minLat, minLon, maxLat, maxLon float64
|
|
|
|
minLat, minLon, maxLat, maxLon, err = bing.QuadKeyToBounds(key)
|
|
|
|
if err != nil {
|
|
|
|
err = errInvalidArgument(key)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
o = 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 == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if vs, sy, ok = tokenval(vs); !ok || sy == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if vs, sz, ok = tokenval(vs); !ok || sz == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var x, y int64
|
|
|
|
var z uint64
|
|
|
|
if x, err = strconv.ParseInt(sx, 10, 64); err != nil {
|
|
|
|
err = errInvalidArgument(sx)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if y, err = strconv.ParseInt(sy, 10, 64); err != nil {
|
|
|
|
err = errInvalidArgument(sy)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if z, err = strconv.ParseUint(sz, 10, 64); err != nil {
|
|
|
|
err = errInvalidArgument(sz)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var minLat, minLon, maxLat, maxLon float64
|
|
|
|
minLat, minLon, maxLat, maxLon = bing.TileXYToBounds(x, y, z)
|
|
|
|
o = geojson.NewRect(geometry.Rect{
|
|
|
|
Min: geometry.Point{X: minLon, Y: minLat},
|
|
|
|
Max: geometry.Point{X: maxLon, Y: maxLat},
|
|
|
|
})
|
|
|
|
case "get":
|
|
|
|
if doClip {
|
2019-02-12 15:33:20 +03:00
|
|
|
err = fmt.Errorf("invalid clip type '%s'", typ)
|
2019-02-09 00:57:29 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
var key, id string
|
|
|
|
if vs, key, ok = tokenval(vs); !ok || key == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if vs, id, ok = tokenval(vs); !ok || id == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
col := s.getCol(key)
|
|
|
|
if col == nil {
|
|
|
|
err = errKeyNotFound
|
|
|
|
return
|
|
|
|
}
|
|
|
|
o, _, ok = col.Get(id)
|
|
|
|
if !ok {
|
|
|
|
err = errIDNotFound
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2019-02-12 15:33:20 +03:00
|
|
|
func (s *Server) cmdTest(msg *Message) (res resp.Value, err error) {
|
2019-02-09 00:57:29 +03:00
|
|
|
start := time.Now()
|
|
|
|
vs := msg.Args[1:]
|
|
|
|
|
|
|
|
var ok bool
|
|
|
|
var test string
|
2019-06-11 00:47:42 +03:00
|
|
|
var clipped geojson.Object
|
|
|
|
var area1, area2 *areaExpression
|
|
|
|
if vs, area1, err = s.parseAreaExpression(vs, false); err != nil {
|
2019-02-09 00:57:29 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if vs, test, ok = tokenval(vs); !ok || test == "" {
|
|
|
|
err = errInvalidNumberOfArguments
|
|
|
|
return
|
|
|
|
}
|
|
|
|
lTest := strings.ToLower(test)
|
|
|
|
if lTest != "within" && lTest != "intersects" {
|
|
|
|
err = errInvalidArgument(test)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
var wtok string
|
|
|
|
var nvs []string
|
|
|
|
var doClip bool
|
|
|
|
nvs, wtok, ok = tokenval(vs)
|
|
|
|
if ok && len(wtok) > 0 {
|
|
|
|
switch strings.ToLower(wtok) {
|
|
|
|
case "clip":
|
|
|
|
vs = nvs
|
|
|
|
if lTest != "intersects" {
|
2019-02-12 15:33:20 +03:00
|
|
|
err = errInvalidArgument(wtok)
|
2019-02-09 00:57:29 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
doClip = true
|
|
|
|
}
|
|
|
|
}
|
2019-06-11 00:47:42 +03:00
|
|
|
if vs, area2, err = s.parseAreaExpression(vs, doClip); err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if doClip && (area1.obj == nil || area2.obj == nil) {
|
|
|
|
err = errInvalidArgument("clip")
|
2019-02-09 00:57:29 +03:00
|
|
|
return
|
|
|
|
}
|
|
|
|
if len(vs) != 0 {
|
|
|
|
err = errInvalidNumberOfArguments
|
2021-07-08 16:46:08 +03:00
|
|
|
return
|
2019-02-09 00:57:29 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
var result int
|
|
|
|
if lTest == "within" {
|
2019-06-11 00:47:42 +03:00
|
|
|
if area1.WithinExpr(area2) {
|
2019-02-09 00:57:29 +03:00
|
|
|
result = 1
|
|
|
|
}
|
|
|
|
} else if lTest == "intersects" {
|
2019-06-11 00:47:42 +03:00
|
|
|
if area1.IntersectsExpr(area2) {
|
2019-02-09 00:57:29 +03:00
|
|
|
result = 1
|
|
|
|
if doClip {
|
2020-03-26 01:35:31 +03:00
|
|
|
clipped = clip.Clip(area1.obj, area2.obj, nil)
|
2019-02-09 00:57:29 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
switch msg.OutputType {
|
|
|
|
case JSON:
|
|
|
|
var buf bytes.Buffer
|
|
|
|
buf.WriteString(`{"ok":true`)
|
2019-02-12 15:33:20 +03:00
|
|
|
if result != 0 {
|
|
|
|
buf.WriteString(`,"result":true`)
|
|
|
|
} else {
|
|
|
|
buf.WriteString(`,"result":false`)
|
|
|
|
}
|
2019-02-09 00:57:29 +03:00
|
|
|
if clipped != nil {
|
|
|
|
buf.WriteString(`,"object":` + clipped.JSON())
|
|
|
|
}
|
2021-07-08 16:46:08 +03:00
|
|
|
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
|
2019-02-09 00:57:29 +03:00
|
|
|
return resp.StringValue(buf.String()), nil
|
|
|
|
case RESP:
|
|
|
|
if clipped != nil {
|
|
|
|
return resp.ArrayValue([]resp.Value{
|
|
|
|
resp.IntegerValue(result),
|
|
|
|
resp.StringValue(clipped.JSON())}), nil
|
|
|
|
}
|
|
|
|
return resp.IntegerValue(result), nil
|
|
|
|
}
|
|
|
|
return NOMessage, nil
|
|
|
|
}
|