Add s2 prefix to all cell ids

Fixes location search when using SQLite

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2020-06-05 16:49:32 +02:00
parent 5d12ad05d7
commit fbf675fbfb
14 changed files with 230 additions and 41 deletions

View file

@ -126,9 +126,9 @@
const photo = this.results[index]; const photo = this.results[index];
if (photo.LocationID) { if (photo.LocationID) {
this.$router.push({name: "place", params: {q: "s2:" + photo.LocationID}}); this.$router.push({name: "place", params: {q: photo.LocationID}});
} else if (photo.PlaceID.length > 3) { } else if (photo.PlaceID.length > 3) {
this.$router.push({name: "place", params: {q: "s2:" + photo.PlaceID}}); this.$router.push({name: "place", params: {q: photo.PlaceID}});
} }
}, },
editPhoto(index) { editPhoto(index) {

View file

@ -166,9 +166,9 @@
const photo = this.results[index]; const photo = this.results[index];
if (photo.LocationID) { if (photo.LocationID) {
this.$router.push({name: "place", params: {q: "s2:" + photo.LocationID}}); this.$router.push({name: "place", params: {q: photo.LocationID}});
} else if (photo.PlaceID.length > 3) { } else if (photo.PlaceID.length > 3) {
this.$router.push({name: "place", params: {q: "s2:" + photo.PlaceID}}); this.$router.push({name: "place", params: {q: photo.PlaceID}});
} }
}, },
editPhoto(index) { editPhoto(index) {

View file

@ -41,7 +41,7 @@ func CreateUnknownLocation() {
func NewLocation(lat, lng float32) *Location { func NewLocation(lat, lng float32) *Location {
result := &Location{} result := &Location{}
result.ID = s2.Token(float64(lat), float64(lng)) result.ID = s2.PrefixedToken(float64(lat), float64(lng))
return result return result
} }
@ -57,7 +57,7 @@ func (m *Location) Find(api string) error {
} }
l := &maps.Location{ l := &maps.Location{
ID: m.ID, ID: s2.NormalizeToken(m.ID),
} }
if err := l.QueryApi(api); err != nil { if err := l.QueryApi(api); err != nil {
@ -65,11 +65,11 @@ func (m *Location) Find(api string) error {
return err return err
} }
if place := FindPlace(l.S2Token(), l.Label()); place != nil { if place := FindPlace(l.PrefixedToken(), l.Label()); place != nil {
m.Place = place m.Place = place
} else { } else {
place = &Place{ place = &Place{
ID: l.S2Token(), ID: l.PrefixedToken(),
LocLabel: l.Label(), LocLabel: l.Label(),
LocCity: l.City(), LocCity: l.City(),
LocState: l.State(), LocState: l.State(),

View file

@ -1,6 +1,10 @@
package entity package entity
import "time" import (
"time"
"github.com/photoprism/photoprism/pkg/s2"
)
type LocationMap map[string]Location type LocationMap map[string]Location
@ -22,7 +26,7 @@ func (m LocationMap) Pointer(name string) *Location {
var LocationFixtures = LocationMap{ var LocationFixtures = LocationMap{
"mexico": { "mexico": {
ID: "85d1ea7d382c", ID: s2.TokenPrefix+"85d1ea7d382c",
PlaceID: PlaceFixtures.Get("mexico").ID, PlaceID: PlaceFixtures.Get("mexico").ID,
LocName: "Adosada Platform", LocName: "Adosada Platform",
LocCategory: "botanical garden", LocCategory: "botanical garden",
@ -32,10 +36,10 @@ var LocationFixtures = LocationMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"caravan park": { "caravan park": {
ID: "1ef75a71a36c", ID: s2.TokenPrefix+"1ef75a71a36c",
PlaceID: "1ef75a71a36c", PlaceID: s2.TokenPrefix+"1ef75a71a36c",
Place: &Place{ Place: &Place{
ID: "1ef75a71a36", ID: "x1ef75a71a36",
LocLabel: "Mandeni, KwaZulu-Natal, South Africa", LocLabel: "Mandeni, KwaZulu-Natal, South Africa",
LocCity: "Mandeni", LocCity: "Mandeni",
LocState: "KwaZulu-Natal", LocState: "KwaZulu-Natal",
@ -50,7 +54,7 @@ var LocationFixtures = LocationMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"zinkwazi": { "zinkwazi": {
ID: "1ef744d1e28c", ID: s2.TokenPrefix+"1ef744d1e28c",
PlaceID: PlaceFixtures.Get("zinkwazi").ID, PlaceID: PlaceFixtures.Get("zinkwazi").ID,
Place: PlaceFixtures.Pointer("zinkwazi"), Place: PlaceFixtures.Pointer("zinkwazi"),
LocName: "Zinkwazi Beach", LocName: "Zinkwazi Beach",
@ -60,7 +64,7 @@ var LocationFixtures = LocationMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"hassloch": { "hassloch": {
ID: "1ef744d1e280", ID: s2.TokenPrefix+"1ef744d1e280",
PlaceID: PlaceFixtures.Get("holidaypark").ID, PlaceID: PlaceFixtures.Get("holidaypark").ID,
Place: PlaceFixtures.Pointer("holidaypark"), Place: PlaceFixtures.Pointer("holidaypark"),
LocName: "Holiday Park", LocName: "Holiday Park",
@ -70,7 +74,7 @@ var LocationFixtures = LocationMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"emptyNameLongCity": { "emptyNameLongCity": {
ID: "1ef744d1e281", ID: s2.TokenPrefix+"1ef744d1e281",
PlaceID: PlaceFixtures.Get("emptyNameLongCity").ID, PlaceID: PlaceFixtures.Get("emptyNameLongCity").ID,
Place: PlaceFixtures.Pointer("emptyNameLongCity"), Place: PlaceFixtures.Pointer("emptyNameLongCity"),
LocName: "", LocName: "",
@ -80,7 +84,7 @@ var LocationFixtures = LocationMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"emptyNameShortCity": { "emptyNameShortCity": {
ID: "1ef744d1e282", ID: s2.TokenPrefix+"1ef744d1e282",
PlaceID: PlaceFixtures.Get("emptyNameShortCity").ID, PlaceID: PlaceFixtures.Get("emptyNameShortCity").ID,
Place: PlaceFixtures.Pointer("emptyNameShortCity"), Place: PlaceFixtures.Pointer("emptyNameShortCity"),
LocName: "", LocName: "",
@ -90,7 +94,7 @@ var LocationFixtures = LocationMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"veryLongLocName": { "veryLongLocName": {
ID: "1ef744d1e283", ID: s2.TokenPrefix+"1ef744d1e283",
PlaceID: PlaceFixtures.Get("veryLongLocName").ID, PlaceID: PlaceFixtures.Get("veryLongLocName").ID,
Place: PlaceFixtures.Pointer("veryLongLocName"), Place: PlaceFixtures.Pointer("veryLongLocName"),
LocName: "longlonglonglonglonglonglonglonglonglonglonglonglongName", LocName: "longlonglonglonglonglonglonglonglonglonglonglonglongName",
@ -100,7 +104,7 @@ var LocationFixtures = LocationMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"mediumLongLocName": { "mediumLongLocName": {
ID: "1ef744d1e283", ID: s2.TokenPrefix+"1ef744d1e283",
PlaceID: PlaceFixtures.Get("mediumLongLocName").ID, PlaceID: PlaceFixtures.Get("mediumLongLocName").ID,
Place: PlaceFixtures.Pointer("mediumLongLocName"), Place: PlaceFixtures.Pointer("mediumLongLocName"),
LocName: "longlonglonglonglonglongName", LocName: "longlonglonglonglonglongName",

View file

@ -1,6 +1,10 @@
package entity package entity
import "time" import (
"time"
"github.com/photoprism/photoprism/pkg/s2"
)
type PlacesMap map[string]Place type PlacesMap map[string]Place
@ -22,7 +26,7 @@ func (m PlacesMap) Pointer(name string) *Place {
var PlaceFixtures = PlacesMap{ var PlaceFixtures = PlacesMap{
"mexico": { "mexico": {
ID: "85d1ea7d3278", ID: s2.TokenPrefix+"85d1ea7d3278",
LocLabel: "Teotihuacán, Mexico, Mexico", LocLabel: "Teotihuacán, Mexico, Mexico",
LocCity: "Teotihuacán", LocCity: "Teotihuacán",
LocState: "State of Mexico", LocState: "State of Mexico",
@ -35,7 +39,7 @@ var PlaceFixtures = PlacesMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"zinkwazi": { "zinkwazi": {
ID: "1ef744d1e279", ID: s2.TokenPrefix+"1ef744d1e279",
LocLabel: "KwaDukuza, KwaZulu-Natal, South Africa", LocLabel: "KwaDukuza, KwaZulu-Natal, South Africa",
LocCity: "KwaDukuza", LocCity: "KwaDukuza",
LocState: "KwaZulu-Natal", LocState: "KwaZulu-Natal",
@ -48,7 +52,7 @@ var PlaceFixtures = PlacesMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"holidaypark": { "holidaypark": {
ID: "1ef744d1e280", ID: s2.TokenPrefix+"1ef744d1e280",
LocLabel: "Holiday Park, Amusement", LocLabel: "Holiday Park, Amusement",
LocCity: "", LocCity: "",
LocState: "Rheinland-Pfalz", LocState: "Rheinland-Pfalz",
@ -61,7 +65,7 @@ var PlaceFixtures = PlacesMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"emptyNameLongCity": { "emptyNameLongCity": {
ID: "1ef744d1e281", ID: s2.TokenPrefix+"1ef744d1e281",
LocLabel: "labelEmptyNameLongCity", LocLabel: "labelEmptyNameLongCity",
LocCity: "longlonglonglonglongcity", LocCity: "longlonglonglonglongcity",
LocState: "Rheinland-Pfalz", LocState: "Rheinland-Pfalz",
@ -74,7 +78,7 @@ var PlaceFixtures = PlacesMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"emptyNameShortCity": { "emptyNameShortCity": {
ID: "1ef744d1e282", ID: s2.TokenPrefix+"1ef744d1e282",
LocLabel: "labelEmptyNameShortCity", LocLabel: "labelEmptyNameShortCity",
LocCity: "shortcity", LocCity: "shortcity",
LocState: "Rheinland-Pfalz", LocState: "Rheinland-Pfalz",
@ -87,7 +91,7 @@ var PlaceFixtures = PlacesMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"veryLongLocName": { "veryLongLocName": {
ID: "1ef744d1e283", ID: s2.TokenPrefix+"1ef744d1e283",
LocLabel: "labelVeryLongLocName", LocLabel: "labelVeryLongLocName",
LocCity: "Mainz", LocCity: "Mainz",
LocState: "Rheinland-Pfalz", LocState: "Rheinland-Pfalz",
@ -100,7 +104,7 @@ var PlaceFixtures = PlacesMap{
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
}, },
"mediumLongLocName": { "mediumLongLocName": {
ID: "1ef744d1e284", ID: s2.TokenPrefix+"1ef744d1e284",
LocLabel: "labelMediumLongLocName", LocLabel: "labelMediumLongLocName",
LocCity: "New york", LocCity: "New york",
LocState: "New york", LocState: "New york",

View file

@ -4,6 +4,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/photoprism/photoprism/pkg/s2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -14,7 +15,7 @@ func TestCreateUnknownPlace(t *testing.T) {
func TestFindPlaceByLabel(t *testing.T) { func TestFindPlaceByLabel(t *testing.T) {
t.Run("find by id", func(t *testing.T) { t.Run("find by id", func(t *testing.T) {
r := FindPlace("1ef744d1e280", "") r := FindPlace(s2.TokenPrefix+"1ef744d1e280", "")
if r == nil { if r == nil {
t.Fatal("result should not be nil") t.Fatal("result should not be nil")
@ -23,7 +24,7 @@ func TestFindPlaceByLabel(t *testing.T) {
assert.Equal(t, "de", r.LocCountry) assert.Equal(t, "de", r.LocCountry)
}) })
t.Run("find by id", func(t *testing.T) { t.Run("find by id", func(t *testing.T) {
r := FindPlace("85d1ea7d3278", "") r := FindPlace(s2.TokenPrefix+"85d1ea7d3278", "")
if r == nil { if r == nil {
t.Fatal("result should not be nil") t.Fatal("result should not be nil")
@ -57,7 +58,7 @@ func TestPlace_Find(t *testing.T) {
}) })
t.Run("record does not exist", func(t *testing.T) { t.Run("record does not exist", func(t *testing.T) {
place := &Place{ place := &Place{
ID: "1110", ID: s2.TokenPrefix+"1110",
LocLabel: "test", LocLabel: "test",
LocCity: "testCity", LocCity: "testCity",
LocState: "", LocState: "",
@ -70,8 +71,8 @@ func TestPlace_Find(t *testing.T) {
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
New: false, New: false,
} }
r := place.Find() err := place.Find()
assert.Equal(t, "record not found", r.Error()) assert.EqualError(t, err, "record not found")
}) })
} }

View file

@ -6,6 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/maps/osm" "github.com/photoprism/photoprism/internal/maps/osm"
"github.com/photoprism/photoprism/internal/maps/places" "github.com/photoprism/photoprism/internal/maps/places"
"github.com/photoprism/photoprism/pkg/s2"
) )
/* TODO /* TODO
@ -156,6 +157,10 @@ func (l Location) S2Token() string {
return l.ID return l.ID
} }
func (l Location) PrefixedToken() string {
return s2.Prefix(l.ID)
}
func (l Location) Name() string { func (l Location) Name() string {
return l.LocName return l.LocName
} }

View file

@ -245,6 +245,14 @@ func TestLocation_S2Token(t *testing.T) {
}) })
} }
func TestLocation_PrefixedToken(t *testing.T) {
t.Run("123", func(t *testing.T) {
l := NewLocation("123", "Indian ocean", "", "", "Nürnberg", "Bayern", "de", "", []string{})
assert.Equal(t, s2.TokenPrefix+"123", l.PrefixedToken())
})
}
func TestLocation_Name(t *testing.T) { func TestLocation_Name(t *testing.T) {
t.Run("Christkindlesmarkt", func(t *testing.T) { t.Run("Christkindlesmarkt", func(t *testing.T) {
l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "", []string{}) l := NewLocation("", "Christkindlesmarkt", "", "", "Nürnberg", "Bayern", "de", "", []string{})

View file

@ -28,6 +28,11 @@ func TestFindLocation(t *testing.T) {
assert.Error(t, err, "places: skipping lat 0.000000, lng 0.000000") assert.Error(t, err, "places: skipping lat 0.000000, lng 0.000000")
t.Log(l) t.Log(l)
}) })
t.Run("short id", func(t *testing.T) {
l, err := FindLocation("ab")
assert.Error(t, err, "places: skipping lat 0.000000, lng 0.000000")
t.Log(l)
})
t.Run("invalid id", func(t *testing.T) { t.Run("invalid id", func(t *testing.T) {
l, err := FindLocation("") l, err := FindLocation("")
assert.Error(t, err, "places: invalid location id ") assert.Error(t, err, "places: invalid location id ")
@ -35,13 +40,13 @@ func TestFindLocation(t *testing.T) {
}) })
t.Run("cached true", func(t *testing.T) { t.Run("cached true", func(t *testing.T) {
var p = NewPlace("1", "", "", "", "de", "") var p = NewPlace("1", "", "", "", "de", "")
location := NewLocation("54", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true) location := NewLocation("1e95998417cc", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true)
l, err := FindLocation(location.ID) l, err := FindLocation(location.ID)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
assert.Equal(t, false, l.Cached) assert.Equal(t, false, l.Cached)
l2, err2 := FindLocation("54") l2, err2 := FindLocation("1e95998417cc")
if err2 != nil { if err2 != nil {
t.Fatal(err2) t.Fatal(err2)
@ -52,9 +57,9 @@ func TestFindLocation(t *testing.T) {
func TestLocationGetters(t *testing.T) { func TestLocationGetters(t *testing.T) {
var p = NewPlace("1", "testLabel", "berlin", "berlin", "de", "foobar") var p = NewPlace("1", "testLabel", "berlin", "berlin", "de", "foobar")
location := NewLocation("54", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true) location := NewLocation("1e95998417cc", 52.51961810676184, 13.40806264572578, "TestLocation", "test", p, true)
t.Run("wrong id", func(t *testing.T) { t.Run("wrong id", func(t *testing.T) {
assert.Equal(t, "54", location.CellID()) assert.Equal(t, "1e95998417cc", location.CellID())
assert.Equal(t, "TestLocation", location.Name()) assert.Equal(t, "TestLocation", location.Name())
assert.Equal(t, "test", location.Category()) assert.Equal(t, "test", location.Category())
assert.Equal(t, "testLabel", location.Label()) assert.Equal(t, "testLabel", location.Label())

View file

@ -5,6 +5,7 @@ import (
"testing" "testing"
"github.com/photoprism/photoprism/internal/config" "github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/s2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -32,7 +33,7 @@ func TestMediaFile_Location(t *testing.T) {
assert.Equal(t, "Hyogo Prefecture", location.State()) assert.Equal(t, "Hyogo Prefecture", location.State())
assert.Equal(t, "Japan", location.CountryName()) assert.Equal(t, "Japan", location.CountryName())
assert.Equal(t, "", location.Category()) assert.Equal(t, "", location.Category())
assert.True(t, strings.HasPrefix(location.ID, "3554df45")) assert.True(t, strings.HasPrefix(location.ID, s2.TokenPrefix+"3554df45"))
location2, err := mediaFile.Location() location2, err := mediaFile.Location()
if err != nil { if err != nil {
@ -67,7 +68,7 @@ func TestMediaFile_Location(t *testing.T) {
assert.Equal(t, "Tübingen", location.City()) assert.Equal(t, "Tübingen", location.City())
assert.Equal(t, "de", location.CountryCode()) assert.Equal(t, "de", location.CountryCode())
assert.Equal(t, "Germany", location.CountryName()) assert.Equal(t, "Germany", location.CountryName())
assert.True(t, strings.HasPrefix(location.ID, "4799e4a5")) assert.True(t, strings.HasPrefix(location.ID, s2.TokenPrefix+"4799e4a5"))
}) })
t.Run("dog_orange.jpg", func(t *testing.T) { t.Run("dog_orange.jpg", func(t *testing.T) {
conf := config.TestConfig() conf := config.TestConfig()

View file

@ -26,6 +26,8 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
s := UnscopedDb() s := UnscopedDb()
// s.LogMode(true)
s = s.Table("photos"). s = s.Table("photos").
Select(`photos.id, photos.photo_uid, photos.photo_type, photos.photo_lat, photos.photo_lng, Select(`photos.id, photos.photo_uid, photos.photo_type, photos.photo_lat, photos.photo_lng,
photos.photo_title, photos.photo_description, photos.photo_favorite, photos.taken_at, files.file_hash, files.file_width, photos.photo_title, photos.photo_description, photos.photo_favorite, photos.taken_at, files.file_hash, files.file_width,
@ -162,10 +164,10 @@ func Geo(f form.GeoSearch) (results GeoResults, err error) {
} }
if f.S2 != "" { if f.S2 != "" {
s2Min, s2Max := s2.Range(f.S2, 7) s2Min, s2Max := s2.PrefixedRange(f.S2, 7)
s = s.Where("photos.location_id BETWEEN ? AND ?", s2Min, s2Max) s = s.Where("photos.location_id BETWEEN ? AND ?", s2Min, s2Max)
} else if f.Olc != "" { } else if f.Olc != "" {
s2Min, s2Max := s2.Range(pluscode.S2(f.Olc), 7) s2Min, s2Max := s2.PrefixedRange(pluscode.S2(f.Olc), 7)
s = s.Where("photos.location_id BETWEEN ? AND ?", s2Min, s2Max) s = s.Where("photos.location_id BETWEEN ? AND ?", s2Min, s2Max)
} else { } else {
// Inaccurate distance search, but probably 'good enough' for now // Inaccurate distance search, but probably 'good enough' for now

44
pkg/s2/prefix.go Normal file
View file

@ -0,0 +1,44 @@
package s2
import (
"strings"
)
var TokenPrefix = "s2:"
// NormalizeToken removes the prefix from a token and converts all characters to lower case.
func NormalizeToken(token string) string {
token = strings.ToLower(token)
token = strings.TrimSpace(token)
if strings.HasPrefix(token, TokenPrefix) {
return token[len(TokenPrefix):]
}
return token
}
// Prefix adds a token prefix if not exists.
func Prefix(token string) string {
if len(token) < 3 {
return token
}
if strings.HasPrefix(token, TokenPrefix) {
return token
}
return TokenPrefix+token
}
// PrefixedToken returns the prefixed S2 cell token for coordinates using the default level.
func PrefixedToken(lat, lng float64) string {
return Prefix(Token(lat, lng))
}
// Range returns a token range for finding nearby locations.
func PrefixedRange(token string, levelUp int) (min, max string) {
min, max = Range(token, levelUp)
return Prefix(min), Prefix(max)
}

111
pkg/s2/prefix_test.go Normal file
View file

@ -0,0 +1,111 @@
package s2
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestNormalizeToken(t *testing.T) {
t.Run(TokenPrefix+"1242342bac", func(t *testing.T) {
input := TokenPrefix+"1242342bac"
output := NormalizeToken(input)
assert.Equal(t, "1242342bac", output)
})
t.Run("abc", func(t *testing.T) {
input := "abc"
output := NormalizeToken(input)
assert.Equal(t, "abc", output)
})
}
func TestPrefix(t *testing.T) {
t.Run(TokenPrefix+"1242342bac", func(t *testing.T) {
input := TokenPrefix+"1242342bac"
output := Prefix(input)
assert.Equal(t, input, output)
})
t.Run("abc", func(t *testing.T) {
input := "1242342bac"
output := Prefix(input)
assert.Equal(t, TokenPrefix+input, output)
})
t.Run("empty string", func(t *testing.T) {
output := Prefix("")
assert.Equal(t, "", output)
})
}
func TestPrefixedToken(t *testing.T) {
t.Run("germany", func(t *testing.T) {
token := PrefixedToken(48.56344833333333, 8.996878333333333)
expected := TokenPrefix+"4799e370"
assert.True(t, strings.HasPrefix(token, expected))
})
t.Run("lat_overflow", func(t *testing.T) {
token := PrefixedToken(548.56344833333333, 8.996878333333333)
expected := ""
assert.Equal(t, expected, token)
})
t.Run("lng_overflow", func(t *testing.T) {
token := PrefixedToken(48.56344833333333, 258.996878333333333)
expected := ""
assert.Equal(t, expected, token)
})
}
func TestPrefixedRange(t *testing.T) {
t.Run("valid_1", func(t *testing.T) {
min, max := PrefixedRange("4799e370ca54c8b9", 1)
assert.Equal(t, TokenPrefix+"4799e370ca54c8b1", min)
assert.Equal(t, TokenPrefix+"4799e370ca54c8c1", max)
})
t.Run("valid_2", func(t *testing.T) {
min, max := PrefixedRange(TokenPrefix+"4799e370ca54c8b9", 2)
assert.Equal(t, TokenPrefix+"4799e370ca54c881", min)
assert.Equal(t, TokenPrefix+"4799e370ca54c8c1", max)
})
t.Run("valid_3", func(t *testing.T) {
min, max := PrefixedRange("4799e370ca54c8b9", 3)
assert.Equal(t, TokenPrefix+"4799e370ca54c801", min)
assert.Equal(t, TokenPrefix+"4799e370ca54c901", max)
})
t.Run("valid_4", func(t *testing.T) {
min, max := PrefixedRange(TokenPrefix+"4799e370ca54c8b9", 4)
assert.Equal(t, TokenPrefix+"4799e370ca54c601", min)
assert.Equal(t, TokenPrefix+"4799e370ca54ca01", max)
})
t.Run("valid_5", func(t *testing.T) {
min, max := PrefixedRange("4799e370ca54c8b9", 5)
assert.Equal(t, TokenPrefix+"4799e370ca54c001", min)
assert.Equal(t, TokenPrefix+"4799e370ca54d001", max)
})
t.Run("invalid", func(t *testing.T) {
min, max := PrefixedRange("4799e370ca5q", 1)
assert.Equal(t, "", min)
assert.Equal(t, "", max)
})
}

View file

@ -44,7 +44,9 @@ func TokenLevel(lat, lng float64, level int) string {
// LatLng returns the coordinates for a S2 cell token. // LatLng returns the coordinates for a S2 cell token.
func LatLng(token string) (lat, lng float64) { func LatLng(token string) (lat, lng float64) {
if token == "" || token == "-" { token = NormalizeToken(token)
if len(token) < 3 {
return 0.0, 0.0 return 0.0, 0.0
} }
@ -65,6 +67,8 @@ func IsZero(lat, lng float64) bool {
// Range returns a token range for finding nearby locations. // Range returns a token range for finding nearby locations.
func Range(token string, levelUp int) (min, max string) { func Range(token string, levelUp int) (min, max string) {
token = NormalizeToken(token)
c := gs2.CellIDFromToken(token) c := gs2.CellIDFromToken(token)
if !c.IsValid() { if !c.IsValid() {