search: GPS location search

Like loc:hawaii or loc:USA or loc:94128, etc.

Change-Id: I11f47bf464a812f0b62e7799752811144bb7454e
This commit is contained in:
Brad Fitzpatrick 2013-12-23 19:11:55 -08:00
parent d759e12b86
commit 7238bd1652
3 changed files with 84 additions and 14 deletions

View File

@ -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.

View File

@ -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

View File

@ -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
}