Merge pull request #552 from rshura/clip-by

Add CLIPBY subcommand to INTERSECTS/WITHIN
This commit is contained in:
Josh 2021-07-10 09:24:57 -07:00 committed by GitHub
commit 1467cba769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 239 additions and 100 deletions

View File

@ -14,6 +14,7 @@ import (
"github.com/tidwall/geojson/geometry" "github.com/tidwall/geojson/geometry"
"github.com/tidwall/resp" "github.com/tidwall/resp"
"github.com/tidwall/tile38/internal/bing" "github.com/tidwall/tile38/internal/bing"
"github.com/tidwall/tile38/internal/clip"
"github.com/tidwall/tile38/internal/deadline" "github.com/tidwall/tile38/internal/deadline"
"github.com/tidwall/tile38/internal/glob" "github.com/tidwall/tile38/internal/glob"
) )
@ -57,6 +58,119 @@ func (s liveFenceSwitches) usingLua() bool {
return len(s.whereevals) > 0 return len(s.whereevals) > 0
} }
func parseRectArea(ltyp string, vs []string) (nvs []string, rect *geojson.Rect, err error) {
var ok bool
switch ltyp {
default:
err = errNotRectangle
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
}
rect = 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)
rect = 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
}
rect = 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)
rect = geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: minLon, Y: minLat},
Max: geometry.Point{X: maxLon, Y: maxLat},
})
}
nvs = vs
return
}
func (server *Server) cmdSearchArgs( func (server *Server) cmdSearchArgs(
fromFenceCmd bool, cmd string, vs []string, types []string, fromFenceCmd bool, cmd string, vs []string, types []string,
) (s liveFenceSwitches, err error) { ) (s liveFenceSwitches, err error) {
@ -170,106 +284,11 @@ func (server *Server) cmdSearchArgs(
if err != nil { if err != nil {
return return
} }
case "bounds": case "bounds", "hash", "tile", "quadkey":
var sminLat, sminLon, smaxlat, smaxlon string vs, s.obj, err = parseRectArea(ltyp, vs)
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
}
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
}
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
}
var minLat, minLon, maxLat, maxLon float64
minLat, minLon, maxLat, maxLon, err = bing.QuadKeyToBounds(key)
if err != nil { if err != nil {
err = errInvalidArgument(key)
return 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 == "" {
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)
s.obj = geojson.NewRect(geometry.Rect{
Min: geometry.Point{X: minLon, Y: minLat},
Max: geometry.Point{X: maxLon, Y: maxLat},
})
case "get": case "get":
if s.clip { if s.clip {
err = errInvalidArgument("cannot clip with get") err = errInvalidArgument("cannot clip with get")
@ -326,9 +345,38 @@ func (server *Server) cmdSearchArgs(
s.roam.scan = scan s.roam.scan = scan
} }
} }
if len(vs) != 0 {
err = errInvalidNumberOfArguments var clip_rect *geojson.Rect
return var tok, ltok string
for len(vs) > 0 {
if vs, tok, ok = tokenval(vs); !ok || tok == "" {
err = errInvalidNumberOfArguments
return
}
if strings.ToLower(tok) != "clipby" {
err = errInvalidNumberOfArguments
return
}
if vs, tok, ok = tokenval(vs); !ok || tok == "" {
err = errInvalidNumberOfArguments
return
}
ltok = strings.ToLower(tok)
switch ltok {
case "bounds", "hash", "tile", "quadkey":
vs, clip_rect, err = parseRectArea(ltok, vs)
if err == errNotRectangle {
err = errInvalidArgument("cannot clipby " + ltok)
return
}
if err != nil {
return
}
s.obj = clip.Clip(s.obj, clip_rect, &server.geomIndexOpts)
default:
err = errInvalidArgument("cannot clipby " + ltok)
return
}
} }
return return
} }

View File

@ -18,6 +18,7 @@ var errIDNotFound = errors.New("id not found")
var errIDAlreadyExists = errors.New("id already exists") var errIDAlreadyExists = errors.New("id already exists")
var errPathNotFound = errors.New("path not found") var errPathNotFound = errors.New("path not found")
var errKeyHasHooksSet = errors.New("key has hooks set") var errKeyHasHooksSet = errors.New("key has hooks set")
var errNotRectangle = errors.New("not a rectangle")
func errInvalidArgument(arg string) error { func errInvalidArgument(arg string) error {
return fmt.Errorf("invalid argument '%s'", arg) return fmt.Errorf("invalid argument '%s'", arg)

View File

@ -13,8 +13,10 @@ func subTestSearch(t *testing.T, mc *mockServer) {
runStep(t, mc, "INTERSECTS_CIRCLE", keys_INTERSECTS_CIRCLE_test) runStep(t, mc, "INTERSECTS_CIRCLE", keys_INTERSECTS_CIRCLE_test)
runStep(t, mc, "WITHIN", keys_WITHIN_test) runStep(t, mc, "WITHIN", keys_WITHIN_test)
runStep(t, mc, "WITHIN_CURSOR", keys_WITHIN_CURSOR_test) runStep(t, mc, "WITHIN_CURSOR", keys_WITHIN_CURSOR_test)
runStep(t, mc, "WITHIN_CLIPBY", keys_WITHIN_CLIPBY_test)
runStep(t, mc, "INTERSECTS", keys_INTERSECTS_test) runStep(t, mc, "INTERSECTS", keys_INTERSECTS_test)
runStep(t, mc, "INTERSECTS_CURSOR", keys_INTERSECTS_CURSOR_test) runStep(t, mc, "INTERSECTS_CURSOR", keys_INTERSECTS_CURSOR_test)
runStep(t, mc, "INTERSECTS_CLIPBY", keys_INTERSECTS_CLIPBY_test)
runStep(t, mc, "SCAN_CURSOR", keys_SCAN_CURSOR_test) runStep(t, mc, "SCAN_CURSOR", keys_SCAN_CURSOR_test)
runStep(t, mc, "SEARCH_CURSOR", keys_SEARCH_CURSOR_test) runStep(t, mc, "SEARCH_CURSOR", keys_SEARCH_CURSOR_test)
runStep(t, mc, "MATCH", keys_MATCH_test) runStep(t, mc, "MATCH", keys_MATCH_test)
@ -145,6 +147,50 @@ func keys_WITHIN_CURSOR_test(mc *mockServer) error {
}) })
} }
func keys_WITHIN_CLIPBY_test(mc *mockServer) error {
jagged := `{
"type":"Polygon",
"coordinates":[[
[-122.47781753540039,37.74655746554895],
[-122.48777389526366,37.7355619376922],
[-122.4707794189453,37.73271097867418],
[-122.46528625488281,37.735969208590504],
[-122.45189666748047,37.73922729512254],
[-122.4565315246582,37.75008654795525],
[-122.46683120727538,37.75307256315459],
[-122.47781753540039,37.74655746554895]
]]
}`
return mc.DoBatch([][]interface{}{
{"SET", "mykey", "point1", "FIELD", "foo", 1, "POINT", 37.73963454585715, -122.4810791015625}, {"OK"},
{"SET", "mykey", "point2", "FIELD", "foo", 2, "POINT", 37.75130811419222, -122.47438430786133}, {"OK"},
{"SET", "mykey", "point3", "FIELD", "foo", 1, "POINT", 37.74816932695052, -122.47713088989258}, {"OK"},
{"SET", "mykey", "point4", "FIELD", "foo", 2, "POINT", 37.74503040657439, -122.47571468353271}, {"OK"},
{"SET", "other", "jagged", "OBJECT", jagged}, {"OK"},
{"WITHIN", "mykey", "IDS", "GET", "other", "jagged"}, {"[0 [point1 point4]]"},
{"WITHIN", "mykey", "IDS", "BOUNDS",
37.737734023260884, -122.47816085815431, 37.74886496155229, -122.45464324951172,
}, {"[0 [point3 point4]]"},
{"WITHIN", "mykey", "IDS", "GET", "other", "jagged", "CLIPBY", "BOUNDS",
37.737734023260884, -122.47816085815431, 37.74886496155229, -122.45464324951172,
}, {"[0 [point4]]"},
{"WITHIN", "mykey", "IDS", "BOUNDS",
37.74411415606583, -122.48034954071045, 37.7536833241461, -122.47163772583008,
}, {"[0 [point2 point3 point4]]"},
{"WITHIN", "mykey", "IDS", "GET", "other", "jagged", "CLIPBY", "BOUNDS",
37.74411415606583, -122.48034954071045, 37.7536833241461, -122.47163772583008,
}, {"[0 [point4]]"},
{"WITHIN", "mykey", "IDS", "GET", "other", "jagged",
"CLIPBY", "BOUNDS",
37.74411415606583, -122.48034954071045, 37.7536833241461, -122.47163772583008,
"CLIPBY", "BOUNDS",
37.737734023260884, -122.47816085815431, 37.74886496155229, -122.45464324951172,
}, {"[0 [point4]]"},
})
}
func keys_INTERSECTS_test(mc *mockServer) error { func keys_INTERSECTS_test(mc *mockServer) error {
return mc.DoBatch([][]interface{}{ return mc.DoBatch([][]interface{}{
{"SET", "mykey", "point1", "POINT", 37.7335, -122.4412}, {"OK"}, {"SET", "mykey", "point1", "POINT", 37.7335, -122.4412}, {"OK"},
@ -195,6 +241,50 @@ func keys_INTERSECTS_test(mc *mockServer) error {
}) })
} }
func keys_INTERSECTS_CLIPBY_test(mc *mockServer) error {
jagged := `{
"type":"Polygon",
"coordinates":[[
[-122.47781753540039,37.74655746554895],
[-122.48777389526366,37.7355619376922],
[-122.4707794189453,37.73271097867418],
[-122.46528625488281,37.735969208590504],
[-122.45189666748047,37.73922729512254],
[-122.4565315246582,37.75008654795525],
[-122.46683120727538,37.75307256315459],
[-122.47781753540039,37.74655746554895]
]]
}`
return mc.DoBatch([][]interface{}{
{"SET", "mykey", "point1", "FIELD", "foo", 1, "POINT", 37.73963454585715, -122.4810791015625}, {"OK"},
{"SET", "mykey", "point2", "FIELD", "foo", 2, "POINT", 37.75130811419222, -122.47438430786133}, {"OK"},
{"SET", "mykey", "point3", "FIELD", "foo", 1, "POINT", 37.74816932695052, -122.47713088989258}, {"OK"},
{"SET", "mykey", "point4", "FIELD", "foo", 2, "POINT", 37.74503040657439, -122.47571468353271}, {"OK"},
{"SET", "other", "jagged", "OBJECT", jagged}, {"OK"},
{"INTERSECTS", "mykey", "IDS", "GET", "other", "jagged"}, {"[0 [point1 point4]]"},
{"INTERSECTS", "mykey", "IDS", "BOUNDS",
37.737734023260884, -122.47816085815431, 37.74886496155229, -122.45464324951172,
}, {"[0 [point3 point4]]"},
{"INTERSECTS", "mykey", "IDS", "GET", "other", "jagged", "CLIPBY", "BOUNDS",
37.737734023260884, -122.47816085815431, 37.74886496155229, -122.45464324951172,
}, {"[0 [point4]]"},
{"INTERSECTS", "mykey", "IDS", "BOUNDS",
37.74411415606583, -122.48034954071045, 37.7536833241461, -122.47163772583008,
}, {"[0 [point2 point3 point4]]"},
{"INTERSECTS", "mykey", "IDS", "GET", "other", "jagged", "CLIPBY", "BOUNDS",
37.74411415606583, -122.48034954071045, 37.7536833241461, -122.47163772583008,
}, {"[0 [point4]]"},
{"INTERSECTS", "mykey", "IDS", "GET", "other", "jagged",
"CLIPBY", "BOUNDS",
37.74411415606583, -122.48034954071045, 37.7536833241461, -122.47163772583008,
"CLIPBY", "BOUNDS",
37.737734023260884, -122.47816085815431, 37.74886496155229, -122.45464324951172,
}, {"[0 [point4]]"},
})
}
func keys_INTERSECTS_CURSOR_test(mc *mockServer) error { func keys_INTERSECTS_CURSOR_test(mc *mockServer) error {
testArea := `{ testArea := `{
"type": "Polygon", "type": "Polygon",