mirror of https://github.com/tidwall/tile38.git
379 lines
9.4 KiB
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
|
|
}
|