From 7238bd1652b3615003410aa230ff42d25873a296 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 23 Dec 2013 19:11:55 -0800 Subject: [PATCH] search: GPS location search Like loc:hawaii or loc:USA or loc:94128, etc. Change-Id: I11f47bf464a812f0b62e7799752811144bb7454e --- pkg/search/expr.go | 54 ++++++++++++++++++++++++++++++++++++++--- pkg/search/expr_test.go | 4 ++- pkg/search/query.go | 40 +++++++++++++++++++++++------- 3 files changed, 84 insertions(+), 14 deletions(-) diff --git a/pkg/search/expr.go b/pkg/search/expr.go index 919469e1e..84efa105f 100644 --- a/pkg/search/expr.go +++ b/pkg/search/expr.go @@ -18,10 +18,14 @@ package search import ( "errors" + "fmt" "log" "regexp" "strconv" "strings" + + "camlistore.org/pkg/context" + "camlistore.org/pkg/geocode" ) var ( @@ -37,7 +41,7 @@ var ( // near:portland") and returns a SearchQuery for that search text. The // Constraint field will always be set. The Limit and Sort may also be // set. -func parseExpression(exp string) (*SearchQuery, error) { +func parseExpression(ctx *context.Context, exp string) (*SearchQuery, error) { base := &Constraint{ Permanode: &PermanodeConstraint{ SkipHidden: true, @@ -62,13 +66,25 @@ func parseExpression(exp string) (*SearchQuery, error) { }, } } - andFile := func(fc *FileConstraint) { - and(&Constraint{ + permOfFile := func(fc *FileConstraint) *Constraint { + return &Constraint{ Permanode: &PermanodeConstraint{ Attr: "camliContent", ValueInSet: &Constraint{File: fc}, }, - }) + } + } + orConst := func(a, b *Constraint) *Constraint { + return &Constraint{ + Logical: &LogicalConstraint{ + Op: "or", + A: a, + B: b, + }, + } + } + andFile := func(fc *FileConstraint) { + and(permOfFile(fc)) } andWHRatio := func(fc *FloatConstraint) { andFile(&FileConstraint{ @@ -134,6 +150,36 @@ func parseExpression(exp string) (*SearchQuery, error) { Height: whIntConstraint(m[1], m[2]), }) } + if strings.HasPrefix(word, "loc:") { + where := strings.TrimPrefix(word, "loc:") + rects, err := geocode.Lookup(ctx, where) + log.Printf("Geocode lookup for %q: %#v", where, rects) + if err != nil { + return nil, err + } + if len(rects) == 0 { + return nil, fmt.Errorf("No location found for %q", where) + } + var locConstraint *Constraint + for i, rect := range rects { + rectConstraint := permOfFile(&FileConstraint{ + IsImage: true, + Location: &LocationConstraint{ + Left: rect.SouthWest.Long, + Right: rect.NorthEast.Long, + Top: rect.NorthEast.Lat, + Bottom: rect.SouthWest.Lat, + }, + }) + if i == 0 { + locConstraint = rectConstraint + } else { + locConstraint = orConst(locConstraint, rectConstraint) + } + } + and(locConstraint) + continue + } log.Printf("Unknown search expression word %q", word) // TODO: finish. better tokenization. non-operator tokens // are text searches, etc. diff --git a/pkg/search/expr_test.go b/pkg/search/expr_test.go index cd68d8659..c19f845db 100644 --- a/pkg/search/expr_test.go +++ b/pkg/search/expr_test.go @@ -21,6 +21,8 @@ import ( "reflect" "strings" "testing" + + "camlistore.org/pkg/context" ) var parseExprTests = []struct { @@ -147,7 +149,7 @@ func TestParseExpression(t *testing.T) { ins = []string{tt.in} } for _, in := range ins { - got, err := parseExpression(in) + got, err := parseExpression(context.TODO(), in) if err != nil { if tt.errContains != "" && strings.Contains(err.Error(), tt.errContains) { continue diff --git a/pkg/search/query.go b/pkg/search/query.go index f65a621bb..bc4200ceb 100644 --- a/pkg/search/query.go +++ b/pkg/search/query.go @@ -177,7 +177,7 @@ func (q *SearchQuery) addContinueConstraint() error { return errors.New("token not valid for query type") } -func (q *SearchQuery) checkValid() (sq *SearchQuery, err error) { +func (q *SearchQuery) checkValid(ctx *context.Context) (sq *SearchQuery, err error) { if q.Limit < 0 { return nil, errors.New("negative limit") } @@ -186,7 +186,7 @@ func (q *SearchQuery) checkValid() (sq *SearchQuery, err error) { } if q.Constraint == nil { if expr := q.Expression; expr != "" { - sq, err := parseExpression(expr) + sq, err := parseExpression(ctx, expr) if err != nil { return nil, fmt.Errorf("Error parsing search expression %q: %v", expr, err) } @@ -294,11 +294,12 @@ type FileConstraint struct { ModTime *TimeConstraint // For images: - IsImage bool `json:"isImage,omitempty"` - EXIF *EXIFConstraint `json:"exif,omitempty"` - Width *IntConstraint `json:"width,omitempty"` - Height *IntConstraint `json:"height,omitempty"` - WHRatio *FloatConstraint `json:"widthHeightRation,omitempty"` + IsImage bool `json:"isImage,omitempty"` + EXIF *EXIFConstraint `json:"exif,omitempty"` // TODO: implement + Width *IntConstraint `json:"width,omitempty"` + Height *IntConstraint `json:"height,omitempty"` + WHRatio *FloatConstraint `json:"widthHeightRation,omitempty"` + Location *LocationConstraint `json:"location,omitempty"` } type DirConstraint struct { @@ -403,6 +404,17 @@ type EXIFConstraint struct { // ISO, Aperature, Camera Make/Model, etc. } +type LocationConstraint struct { + Top float64 + Left float64 + Right float64 + Bottom float64 +} + +func (c *LocationConstraint) matchesLatLong(lat, long float64) bool { + return c.Left <= long && long <= c.Right && c.Bottom <= lat && lat <= c.Top +} + // A StringConstraint specifies constraints on a string. // All non-zero must match. type StringConstraint struct { @@ -585,7 +597,8 @@ func optimizePlan(c *Constraint) *Constraint { } func (h *Handler) Query(rawq *SearchQuery) (*SearchResult, error) { - exprResult, err := rawq.checkValid() + ctx := context.TODO() // TODO: set from rawq + exprResult, err := rawq.checkValid(ctx) if err != nil { return nil, fmt.Errorf("Invalid SearchQuery: %v", err) } @@ -1084,9 +1097,9 @@ func (c *FileConstraint) blobMatches(s *search, br blob.Ref, bm camtypes.BlobMet return false, nil } } + corpus := s.h.corpus var width, height int64 if c.Width != nil || c.Height != nil || c.WHRatio != nil { - corpus := s.h.corpus if corpus == nil { return false, nil } @@ -1106,6 +1119,15 @@ func (c *FileConstraint) blobMatches(s *search, br blob.Ref, bm camtypes.BlobMet if c.WHRatio != nil && !c.WHRatio.floatMatches(float64(width)/float64(height)) { return false, nil } + if c.Location != nil { + if corpus == nil { + return false, nil + } + lat, long, ok := corpus.FileLatLongLocked(br) + if !ok || !c.Location.matchesLatLong(lat, long) { + return false, nil + } + } // TOOD: EXIF timeconstraint return true, nil }