diff --git a/internal/server/search.go b/internal/server/search.go index e6d7c6d1..75e59d5d 100644 --- a/internal/server/search.go +++ b/internal/server/search.go @@ -14,6 +14,7 @@ import ( "github.com/tidwall/geojson/geometry" "github.com/tidwall/resp" "github.com/tidwall/tile38/internal/bing" + "github.com/tidwall/tile38/internal/clip" "github.com/tidwall/tile38/internal/deadline" "github.com/tidwall/tile38/internal/glob" ) @@ -57,6 +58,119 @@ func (s liveFenceSwitches) usingLua() bool { 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( fromFenceCmd bool, cmd string, vs []string, types []string, ) (s liveFenceSwitches, err error) { @@ -170,106 +284,11 @@ func (server *Server) cmdSearchArgs( 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 - } - 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) + case "bounds", "hash", "tile", "quadkey": + vs, s.obj, err = parseRectArea(ltyp, vs) if err != nil { - err = errInvalidArgument(key) 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": if s.clip { err = errInvalidArgument("cannot clip with get") @@ -326,9 +345,38 @@ func (server *Server) cmdSearchArgs( s.roam.scan = scan } } - if len(vs) != 0 { - err = errInvalidNumberOfArguments - return + + var clip_rect *geojson.Rect + 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 } diff --git a/internal/server/token.go b/internal/server/token.go index b010dc4c..b53d489b 100644 --- a/internal/server/token.go +++ b/internal/server/token.go @@ -18,6 +18,7 @@ var errIDNotFound = errors.New("id not found") var errIDAlreadyExists = errors.New("id already exists") var errPathNotFound = errors.New("path not found") var errKeyHasHooksSet = errors.New("key has hooks set") +var errNotRectangle = errors.New("not a rectangle") func errInvalidArgument(arg string) error { return fmt.Errorf("invalid argument '%s'", arg) diff --git a/tests/keys_search_test.go b/tests/keys_search_test.go index ad225608..8b5d3fa2 100644 --- a/tests/keys_search_test.go +++ b/tests/keys_search_test.go @@ -13,8 +13,10 @@ func subTestSearch(t *testing.T, mc *mockServer) { runStep(t, mc, "INTERSECTS_CIRCLE", keys_INTERSECTS_CIRCLE_test) runStep(t, mc, "WITHIN", keys_WITHIN_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_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, "SEARCH_CURSOR", keys_SEARCH_CURSOR_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 { return mc.DoBatch([][]interface{}{ {"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 { testArea := `{ "type": "Polygon",