tile38/internal/server/test.go

379 lines
9.4 KiB
Go

package server
// TEST command: spatial tests without walking the tree.
import (
"bytes"
"fmt"
"strconv"
"strings"
"time"
"github.com/iwpnd/sectr"
"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"
)
func (s *Server) parseArea(ovs []string, doClip bool) (vs []string, o geojson.Object, err error) {
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 "sector":
if doClip {
err = fmt.Errorf("invalid clip type '%s'", typ)
return
}
var slat, slon, smeters, sb1, sb2 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 = errInvalidNumberOfArguments
return
}
if vs, sb1, ok = tokenval(vs); !ok || sb1 == "" {
err = errInvalidNumberOfArguments
return
}
if vs, sb2, ok = tokenval(vs); !ok || sb2 == "" {
err = errInvalidNumberOfArguments
return
}
var lat, lon, meters, b1, b2 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 meters, err = strconv.ParseFloat(smeters, 64); err != nil {
err = errInvalidArgument(smeters)
return
}
if b1, err = strconv.ParseFloat(sb1, 64); err != nil {
err = errInvalidArgument(sb1)
return
}
if b2, err = strconv.ParseFloat(sb2, 64); err != nil {
err = errInvalidArgument(sb2)
return
}
if b1 == b2 {
err = fmt.Errorf("equal bearings (%s == %s), use CIRCLE instead", sb1, sb2)
return
}
origin := sectr.Point{Lng: lon, Lat: lat}
sector := sectr.NewSector(origin, meters, b1, b2)
o, err = geojson.Parse(string(sector.JSON()), &s.geomParseOpts)
if err != nil {
return
}
case "circle":
if doClip {
err = fmt.Errorf("invalid clip type '%s'", typ)
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 {
err = fmt.Errorf("invalid clip type '%s'", typ)
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 {
err = fmt.Errorf("invalid clip type '%s'", typ)
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.cols.Get(key)
if col == nil {
err = errKeyNotFound
return
}
obj := col.Get(id)
if obj == nil {
err = errIDNotFound
return
}
o = obj.Geo()
}
return
}
// TEST (POINT lat lon)|(GET key id)|(BOUNDS minlat minlon maxlat maxlon)|
// (OBJECT geojson)|(CIRCLE lat lon meters)|(TILE x y z)|(QUADKEY quadkey)|
// (HASH geohash) INTERSECTS|WITHIN [CLIP] (POINT lat lon)|(GET key id)|
// (BOUNDS minlat minlon maxlat maxlon)|(OBJECT geojson)|
// (CIRCLE lat lon meters)|(TILE x y z)|(QUADKEY quadkey)|(HASH geohash)|
// (SECTOR lat lon meters bearing1 bearing2)
func (s *Server) cmdTEST(msg *Message) (res resp.Value, err error) {
start := time.Now()
vs := msg.Args[1:]
var ok bool
var test string
var clipped geojson.Object
var area1, area2 *areaExpression
if vs, area1, err = s.parseAreaExpression(vs, false); err != nil {
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" {
err = errInvalidArgument(wtok)
return
}
doClip = true
}
}
if vs, area2, err = s.parseAreaExpression(vs, doClip); err != nil {
return
}
if doClip && (area1.obj == nil || area2.obj == nil) {
err = errInvalidArgument("clip")
return
}
if len(vs) != 0 {
err = errInvalidNumberOfArguments
return
}
var result int
if lTest == "within" {
if area1.WithinExpr(area2) {
result = 1
}
} else if lTest == "intersects" {
if area1.IntersectsExpr(area2) {
result = 1
if doClip {
clipped = clip.Clip(area1.obj, area2.obj, nil)
}
}
}
if msg.OutputType == JSON {
var buf bytes.Buffer
buf.WriteString(`{"ok":true`)
if result != 0 {
buf.WriteString(`,"result":true`)
} else {
buf.WriteString(`,"result":false`)
}
if clipped != nil {
buf.WriteString(`,"object":` + clipped.JSON())
}
buf.WriteString(`,"elapsed":"` + time.Since(start).String() + "\"}")
return resp.StringValue(buf.String()), nil
}
if clipped != nil {
return resp.ArrayValue([]resp.Value{
resp.IntegerValue(result),
resp.StringValue(clipped.JSON())}), nil
}
return resp.IntegerValue(result), nil
}