From 6da8bd098ab712e0d543c76c76d04755f1f02e09 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Tue, 31 Dec 2019 07:16:11 +0100 Subject: [PATCH] Backend: Add support for new Places API #173 Signed-off-by: Michael Mayer --- assets/config/labels.yml | 4 +- docker/photoprism/Dockerfile | 2 +- internal/entity/location.go | 19 +--- internal/maps/location.go | 64 +++++------- internal/maps/location_test.go | 145 +++++++++++--------------- internal/maps/osm/categories_test.go | 8 +- internal/maps/osm/category.go | 8 +- internal/maps/osm/location.go | 96 ++++++++--------- internal/maps/osm/location_test.go | 50 ++++----- internal/maps/osm/title.go | 10 +- internal/maps/osm/title_test.go | 12 +-- internal/maps/places/cache.go | 9 ++ internal/maps/places/location.go | 121 +++++++++++++++++++++ internal/maps/places/location_test.go | 27 +++++ internal/maps/places/place.go | 10 ++ internal/maps/places/places.go | 14 +++ internal/maps/s2.go | 26 ----- internal/maps/s2_test.go | 88 ---------------- internal/photoprism/location_test.go | 7 +- internal/s2/s2.go | 44 ++++++++ internal/s2/s2_test.go | 89 ++++++++++++++++ 21 files changed, 492 insertions(+), 361 deletions(-) create mode 100644 internal/maps/places/cache.go create mode 100644 internal/maps/places/location.go create mode 100644 internal/maps/places/location_test.go create mode 100644 internal/maps/places/place.go create mode 100644 internal/maps/places/places.go delete mode 100644 internal/maps/s2.go delete mode 100644 internal/maps/s2_test.go create mode 100644 internal/s2/s2.go create mode 100644 internal/s2/s2_test.go diff --git a/assets/config/labels.yml b/assets/config/labels.yml index e5e22ecd7..c7ade2074 100644 --- a/assets/config/labels.yml +++ b/assets/config/labels.yml @@ -475,10 +475,8 @@ american alligator: - alligator triceratops: + label: animal priority: -1 - categories: - - reptile - - animal snake: label: snake diff --git a/docker/photoprism/Dockerfile b/docker/photoprism/Dockerfile index bea22bccb..406471b0c 100644 --- a/docker/photoprism/Dockerfile +++ b/docker/photoprism/Dockerfile @@ -7,7 +7,7 @@ COPY . . # Build PhotoPrism RUN make dep build-js install -# Base base image as photoprism/development +# Same base image as photoprism/development FROM ubuntu:18.04 # Set environment variables diff --git a/internal/entity/location.go b/internal/entity/location.go index dfad57dc9..0402436be 100644 --- a/internal/entity/location.go +++ b/internal/entity/location.go @@ -6,6 +6,7 @@ import ( "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/internal/maps" + "github.com/photoprism/photoprism/internal/s2" "github.com/photoprism/photoprism/internal/util" ) @@ -14,8 +15,6 @@ type Location struct { ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;"` PlaceID string `gorm:"type:varbinary(16);"` Place *Place - LocLat float64 - LocLng float64 LocName string `gorm:"type:varchar(100);"` LocCategory string `gorm:"type:varchar(50);"` LocSuburb string `gorm:"type:varchar(100);"` @@ -27,9 +26,7 @@ type Location struct { func NewLocation(lat, lng float64) *Location { result := &Location{} - result.ID = maps.S2Token(lat, lng) - result.LocLat = lat - result.LocLng = lng + result.ID = s2.Token(lat, lng) return result } @@ -42,11 +39,9 @@ func (m *Location) Find(db *gorm.DB) error { l := &maps.Location{ ID: m.ID, - LocLat: m.LocLat, - LocLng: m.LocLng, } - if err := l.Query(); err != nil { + if err := l.QueryPlaces(); err != nil { return err } @@ -93,14 +88,6 @@ func (m *Location) Unknown() bool { return m.ID == "" } -func (m *Location) Latitude() float64 { - return m.LocLat -} - -func (m *Location) Longitude() float64 { - return m.LocLng -} - func (m *Location) Name() string { return m.LocName } diff --git a/internal/maps/location.go b/internal/maps/location.go index 46df936ea..bb9d9b04f 100644 --- a/internal/maps/location.go +++ b/internal/maps/location.go @@ -5,6 +5,7 @@ import ( "strings" "github.com/photoprism/photoprism/internal/maps/osm" + "github.com/photoprism/photoprism/internal/maps/places" ) /* TODO @@ -24,8 +25,6 @@ ORDER BY loc_country, album_name, taken_year; // Photo location type Location struct { ID string - LocLat float64 - LocLng float64 LocName string LocCategory string LocSuburb string @@ -37,9 +36,8 @@ type Location struct { } type LocationSource interface { + CellID() string CountryCode() string - Latitude() float64 - Longitude() float64 Category() string Name() string City() string @@ -48,47 +46,53 @@ type LocationSource interface { Source() string } -func NewLocation(lat, lng float64) *Location { - id := S2Token(lat, lng) - +func NewLocation(id string) *Location { result := &Location{ - ID: id, - LocLat: lat, - LocLng: lng, + ID: id, } return result } -func (l *Location) Query() error { - o, err := osm.FindLocation(l.LocLat, l.LocLng) +func (l *Location) QueryPlaces() error { + s, err := places.FindLocation(l.ID) if err != nil { return err } - return l.Assign(o) + l.LocSource = s.Source() + l.LocName = s.Name() + l.LocCity = s.City() + l.LocSuburb = s.Suburb() + l.LocState = s.State() + l.LocCountry = s.CountryCode() + l.LocCategory = s.Category() + l.LocLabel = s.Label() + + return nil +} + +func (l *Location) QueryOSM() error { + s, err := osm.FindLocation(l.ID) + + if err != nil { + return err + } + + return l.Assign(s) } func (l *Location) Assign(s LocationSource) error { l.LocSource = s.Source() - if l.LocLat == 0 { - l.LocLat = s.Latitude() - } - if l.LocLng == 0 { - l.LocLng = s.Longitude() - } + l.ID = s.CellID() if l.Unknown() { l.LocCategory = "unknown" return errors.New("maps: unknown location") } - if l.ID == "" { - l.ID = S2Token(l.LocLat, l.LocLng) - } - l.LocName = s.Name() l.LocCity = s.City() l.LocSuburb = s.Suburb() @@ -101,11 +105,7 @@ func (l *Location) Assign(s LocationSource) error { } func (l *Location) Unknown() bool { - if l.LocLng == 0.0 && l.LocLat == 0.0 { - return true - } - - return false + return l.ID == "" } func (l *Location) label() string { @@ -133,14 +133,6 @@ func (l *Location) label() string { return strings.Join(loc[:], ", ") } -func (l Location) Latitude() float64 { - return l.LocLat -} - -func (l Location) Longitude() float64 { - return l.LocLng -} - func (l Location) Name() string { return l.LocName } diff --git a/internal/maps/location_test.go b/internal/maps/location_test.go index 8f624d405..bf84f5d7f 100644 --- a/internal/maps/location_test.go +++ b/internal/maps/location_test.go @@ -1,20 +1,40 @@ package maps import ( + "strings" "testing" "github.com/photoprism/photoprism/internal/maps/osm" + "github.com/photoprism/photoprism/internal/s2" "github.com/stretchr/testify/assert" ) -func TestLocation_Query(t *testing.T) { +func TestLocation_QueryPlaces(t *testing.T) { + t.Run("U Berliner Rathaus", func(t *testing.T) { + lat := 52.51961810676184 + lng := 13.40806264572578 + id := s2.Token(lat, lng) + + l := NewLocation(id) + + if err := l.QueryPlaces(); err != nil { + t.Fatal(err) + } + + assert.Equal(t, "U Berliner Rathaus", l.LocName) + assert.Equal(t, "Berlin, Germany", l.LocLabel) + }) +} + +func TestLocation_QueryOSM(t *testing.T) { t.Run("BerlinFernsehturm", func(t *testing.T) { lat := 52.5208 lng := 13.40953 + id := s2.Token(lat, lng) - l := NewLocation(lat, lng) + l := NewLocation(id) - if err := l.Query(); err != nil { + if err := l.QueryOSM(); err != nil { t.Fatal(err) } @@ -27,8 +47,9 @@ func TestLocation_Assign(t *testing.T) { t.Run("BerlinFernsehturm", func(t *testing.T) { lat := 52.5208 lng := 13.40953 + id := s2.Token(lat, lng) - o, err := osm.FindLocation(lat, lng) + o, err := osm.FindLocation(id) if err != nil { t.Fatal(err) @@ -54,8 +75,9 @@ func TestLocation_Assign(t *testing.T) { t.Run("SantaMonica", func(t *testing.T) { lat := 34.00909444444444 lng := -118.49700833333334 + id := s2.Token(lat, lng) - o, err := osm.FindLocation(lat, lng) + o, err := osm.FindLocation(id) if err != nil { t.Fatal(err) @@ -82,8 +104,9 @@ func TestLocation_Assign(t *testing.T) { t.Run("AirportZurich", func(t *testing.T) { lat := 47.45401666666667 lng := 8.557494444444446 + id := s2.Token(lat, lng) - o, err := osm.FindLocation(lat, lng) + o, err := osm.FindLocation(id) if err != nil { t.Fatal(err) @@ -111,8 +134,9 @@ func TestLocation_Assign(t *testing.T) { t.Run("AirportTegel", func(t *testing.T) { lat := 52.559864397033024 lng := 13.28895092010498 + id := s2.Token(lat, lng) - o, err := osm.FindLocation(lat, lng) + o, err := osm.FindLocation(id) if err != nil { t.Fatal(err) @@ -140,8 +164,9 @@ func TestLocation_Assign(t *testing.T) { t.Run("PinkBeach", func(t *testing.T) { lat := 35.26967222222222 lng := 23.53711666666667 + id := s2.Token(lat, lng) - o, err := osm.FindLocation(lat, lng) + o, err := osm.FindLocation(id) if err != nil { t.Fatal(err) @@ -162,7 +187,7 @@ func TestLocation_Assign(t *testing.T) { t.Fatal(err) } - assert.Equal(t, "149ce7854", l.ID) + assert.True(t, strings.HasPrefix(l.ID, "149ce785")) assert.Equal(t, "Pink Beach", l.LocName) assert.Equal(t, "Chrisoskalitissa, Crete, Greece", l.LocLabel) }) @@ -170,8 +195,9 @@ func TestLocation_Assign(t *testing.T) { t.Run("NewJersey", func(t *testing.T) { lat := 40.74290 lng := -74.04862 + id := s2.Token(lat, lng) - o, err := osm.FindLocation(lat, lng) + o, err := osm.FindLocation(id) if err != nil { t.Fatal(err) @@ -192,7 +218,7 @@ func TestLocation_Assign(t *testing.T) { t.Fatal(err) } - assert.Equal(t, "89c25741c", l.ID) + assert.True(t, strings.HasPrefix(l.ID, "89c25741")) assert.Equal(t, "", l.LocName) assert.Equal(t, "Jersey City, New Jersey, USA", l.LocLabel) }) @@ -200,8 +226,9 @@ func TestLocation_Assign(t *testing.T) { t.Run("SouthAfrica", func(t *testing.T) { lat := -31.976301666666668 lng := 29.148046666666666 + id := s2.Token(lat, lng) - o, err := osm.FindLocation(lat, lng) + o, err := osm.FindLocation(id) if err != nil { t.Fatal(err) @@ -222,7 +249,7 @@ func TestLocation_Assign(t *testing.T) { t.Fatal(err) } - assert.Equal(t, "1e5e4205c", l.ID) + assert.True(t, strings.HasPrefix(l.ID, "1e5e4205")) assert.Equal(t, "R411", l.LocName) assert.Equal(t, "Eastern Cape, South Africa", l.LocLabel) }) @@ -230,11 +257,14 @@ func TestLocation_Assign(t *testing.T) { t.Run("Unknown", func(t *testing.T) { lat := -21.976301666666668 lng := 49.148046666666666 + id := s2.Token(lat, lng) + log.Printf("ID: %s", id) + o, err := osm.FindLocation(id) - o, err := osm.FindLocation(lat, lng) + log.Printf("Output: %+v", o) - if err != nil { - t.Fatal(err) + if err == nil { + t.Fatal("expected error") } assert.False(t, o.Cached) @@ -250,16 +280,18 @@ func TestLocation_Unknown(t *testing.T) { t.Run("true", func(t *testing.T) { lat := 0.0 lng := 0.0 + id := s2.Token(lat, lng) - l := NewLocation(lat, lng) + l := NewLocation(id) assert.Equal(t, true, l.Unknown()) }) t.Run("false", func(t *testing.T) { lat := -31.976301666666668 lng := 29.148046666666666 + id := s2.Token(lat, lng) - l := NewLocation(lat, lng) + l := NewLocation(id) assert.Equal(t, false, l.Unknown()) }) @@ -269,49 +301,22 @@ func TestLocation_place(t *testing.T) { t.Run("unknown", func(t *testing.T) { lat := 0.0 lng := 0.0 + id := s2.Token(lat, lng) - l := NewLocation(lat, lng) + l := NewLocation(id) assert.Equal(t, "Unknown", l.label()) }) t.Run("Nürnberg, Bayern, Germany", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 + l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern"} - l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern"} - - assert.Equal(t, "Nürnberg, Bayern, Germany", l.label()) - }) -} - -func TestLocation_Latitude(t *testing.T) { - t.Run("-31.976301666666668", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern"} - - assert.Equal(t, -31.976301666666668, l.Latitude()) - }) -} - -func TestLocation_Longitude(t *testing.T) { - t.Run("29.148046666666666", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern"} - - assert.Equal(t, 29.148046666666666, l.Longitude()) + assert.Equal(t, "Unknown", l.label()) }) } func TestLocation_Name(t *testing.T) { t.Run("Christkindlesmarkt", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt"} + l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt"} assert.Equal(t, "Christkindlesmarkt", l.Name()) }) @@ -319,10 +324,7 @@ func TestLocation_Name(t *testing.T) { func TestLocation_City(t *testing.T) { t.Run("Nürnberg", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt"} + l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt"} assert.Equal(t, "Nürnberg", l.City()) }) @@ -330,10 +332,7 @@ func TestLocation_City(t *testing.T) { func TestLocation_Suburb(t *testing.T) { t.Run("Hauptmarkt", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} + l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} assert.Equal(t, "Hauptmarkt", l.Suburb()) }) @@ -341,10 +340,7 @@ func TestLocation_Suburb(t *testing.T) { func TestLocation_State(t *testing.T) { t.Run("Bayern", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} + l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} assert.Equal(t, "Bayern", l.State()) }) @@ -352,10 +348,7 @@ func TestLocation_State(t *testing.T) { func TestLocation_Category(t *testing.T) { t.Run("test", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} + l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} assert.Equal(t, "test", l.Category()) }) @@ -363,10 +356,7 @@ func TestLocation_Category(t *testing.T) { func TestLocation_Source(t *testing.T) { t.Run("source", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt", LocSource: "source"} + l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt", LocSource: "source"} assert.Equal(t, "source", l.Source()) }) @@ -374,10 +364,7 @@ func TestLocation_Source(t *testing.T) { func TestLocation_Place(t *testing.T) { t.Run("test-label", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} + l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} assert.Equal(t, "test-label", l.Label()) }) @@ -385,10 +372,7 @@ func TestLocation_Place(t *testing.T) { func TestLocation_CountryCode(t *testing.T) { t.Run("de", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} + l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} assert.Equal(t, "de", l.CountryCode()) }) @@ -396,10 +380,7 @@ func TestLocation_CountryCode(t *testing.T) { func TestLocation_CountryName(t *testing.T) { t.Run("Germany", func(t *testing.T) { - lat := -31.976301666666668 - lng := 29.148046666666666 - - l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} + l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} assert.Equal(t, "Germany", l.CountryName()) }) diff --git a/internal/maps/osm/categories_test.go b/internal/maps/osm/categories_test.go index e325d4a7c..0d20bd117 100644 --- a/internal/maps/osm/categories_test.go +++ b/internal/maps/osm/categories_test.go @@ -9,25 +9,25 @@ import ( func TestOSM_Category(t *testing.T) { t.Run("hill", func(t *testing.T) { - l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "natural", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name"} assert.Equal(t, "hill", l.Category()) }) t.Run("water", func(t *testing.T) { - l := &Location{LocCategory: "", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "water", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "", LocName: "Nice title", LocType: "water", LocDisplayName: "dipslay name"} assert.Equal(t, "water", l.Category()) }) t.Run("shop", func(t *testing.T) { - l := &Location{LocCategory: "shop", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "shop", LocName: "Nice title", LocType: "", LocDisplayName: "dipslay name"} assert.Equal(t, "shop", l.Category()) }) t.Run("no label found", func(t *testing.T) { - l := &Location{LocCategory: "xxx", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "xxx", LocName: "Nice title", LocType: "", LocDisplayName: "dipslay name"} assert.Equal(t, "", l.Category()) }) } diff --git a/internal/maps/osm/category.go b/internal/maps/osm/category.go index 6e5a721f4..15928cdb9 100644 --- a/internal/maps/osm/category.go +++ b/internal/maps/osm/category.go @@ -2,10 +2,10 @@ package osm import "fmt" -func (o Location) Category() (result string) { - key := fmt.Sprintf("%s=%s", o.LocCategory, o.LocType) - catKey := fmt.Sprintf("%s=*", o.LocCategory) - typeKey := fmt.Sprintf("*=%s", o.LocType) +func (l Location) Category() (result string) { + key := fmt.Sprintf("%s=%s", l.LocCategory, l.LocType) + catKey := fmt.Sprintf("%s=*", l.LocCategory) + typeKey := fmt.Sprintf("*=%s", l.LocType) if result, ok := osmCategories[key]; ok { return result diff --git a/internal/maps/osm/location.go b/internal/maps/osm/location.go index 701778215..9423205fd 100644 --- a/internal/maps/osm/location.go +++ b/internal/maps/osm/location.go @@ -2,20 +2,20 @@ package osm import ( "encoding/json" + "errors" "fmt" "net/http" - "strconv" "strings" "time" "github.com/melihmucuk/geocache" + "github.com/photoprism/photoprism/internal/s2" "github.com/photoprism/photoprism/internal/util" ) type Location struct { + ID string `json:"-"` PlaceID int `json:"place_id"` - LocLat string `json:"lat"` - LocLng string `json:"lon"` LocName string `json:"name"` LocCategory string `json:"category"` LocType string `json:"type"` @@ -27,7 +27,13 @@ type Location struct { var ReverseLookupURL = "https://nominatim.openstreetmap.org/reverse?lat=%f&lon=%f&format=jsonv2&accept-language=en&zoom=18" // API docs see https://wiki.openstreetmap.org/wiki/Nominatim#Reverse_Geocoding -func FindLocation(lat, lng float64) (result Location, err error) { +func FindLocation(id string) (result Location, err error) { + if len(id) > 16 || len(id) == 0 { + return result, errors.New("osm: invalid location id") + } + + lat, lng := s2.LatLng(id) + if lat == 0.0 || lng == 0.0 { return result, fmt.Errorf("osm: skipping lat %f, lng %f", lat, lng) } @@ -59,6 +65,14 @@ func FindLocation(lat, lng float64) (result Location, err error) { return result, err } + if result.PlaceID == 0 { + result.ID = "" + + return result, fmt.Errorf("osm: no result for %s", id) + } + + result.ID = id + geoCache.Set(point, result, time.Hour) result.Cached = false @@ -66,23 +80,27 @@ func FindLocation(lat, lng float64) (result Location, err error) { return result, nil } -func (o Location) State() (result string) { - result = o.Address.State +func (l Location) CellID() (result string) { + return l.ID +} + +func (l Location) State() (result string) { + result = l.Address.State return strings.TrimSpace(result) } -func (o Location) City() (result string) { - if o.Address.City != "" { - result = o.Address.City - } else if o.Address.Town != "" { - result = o.Address.Town - } else if o.Address.Village != "" { - result = o.Address.Village - } else if o.Address.County != "" { - result = o.Address.County - } else if o.Address.State != "" { - result = o.Address.State +func (l Location) City() (result string) { + if l.Address.City != "" { + result = l.Address.City + } else if l.Address.Town != "" { + result = l.Address.Town + } else if l.Address.Village != "" { + result = l.Address.Village + } else if l.Address.County != "" { + result = l.Address.County + } else if l.Address.State != "" { + result = l.Address.State } if len([]rune(result)) > 19 { @@ -92,52 +110,22 @@ func (o Location) City() (result string) { return strings.TrimSpace(result) } -func (o Location) Suburb() (result string) { - result = o.Address.Suburb +func (l Location) Suburb() (result string) { + result = l.Address.Suburb return strings.TrimSpace(result) } -func (o Location) CountryCode() (result string) { - result = o.Address.CountryCode +func (l Location) CountryCode() (result string) { + result = l.Address.CountryCode return strings.ToLower(strings.TrimSpace(result)) } -func (o Location) Latitude() (result float64) { - if o.LocLat == "" { - log.Warn("osm: no latitude") - return 0.0 - } - - result, err := strconv.ParseFloat(o.LocLat, 64) - - if err != nil { - log.Errorf("osm: %s", err.Error()) - } - - return result +func (l Location) Keywords() (result []string) { + return util.Keywords(l.LocDisplayName) } -func (o Location) Longitude() (result float64) { - if o.LocLng == "" { - log.Warn("osm: no longitude") - return 0.0 - } - - result, err := strconv.ParseFloat(o.LocLng, 64) - - if err != nil { - log.Errorf("osm: %s", err.Error()) - } - - return result -} - -func (o Location) Keywords() (result []string) { - return util.Keywords(o.LocDisplayName) -} - -func (o Location) Source() string { +func (l Location) Source() string { return "osm" } diff --git a/internal/maps/osm/location_test.go b/internal/maps/osm/location_test.go index 50c94afdb..223bb3d07 100644 --- a/internal/maps/osm/location_test.go +++ b/internal/maps/osm/location_test.go @@ -3,15 +3,17 @@ package osm import ( "testing" + "github.com/photoprism/photoprism/internal/s2" "github.com/stretchr/testify/assert" ) func TestFindLocation(t *testing.T) { - t.Run("BerlinFernsehturm", func(t *testing.T) { + t.Run("Fernsehturm Berlin 1", func(t *testing.T) { lat := 52.5208 lng := 13.40953 + id := s2.Token(lat, lng) - l, err := FindLocation(lat, lng) + l, err := FindLocation(id) if err != nil { t.Fatal(err) @@ -29,7 +31,7 @@ func TestFindLocation(t *testing.T) { assert.Equal(t, 123456, l.PlaceID) - cached, err := FindLocation(lat, lng) + cached, err := FindLocation(id) if err != nil { t.Fatal(err) @@ -44,19 +46,20 @@ func TestFindLocation(t *testing.T) { assert.Equal(t, l.Address.Country, cached.Address.Country) }) - t.Run("BerlinMuseum", func(t *testing.T) { + t.Run("Fernsehturm Berlin 2", func(t *testing.T) { lat := 52.52057 lng := 13.40889 + id := s2.Token(lat, lng) - l, err := FindLocation(lat, lng) + l, err := FindLocation(id) if err != nil { t.Fatal(err) } assert.False(t, l.Cached) - assert.Equal(t, 48287001, l.PlaceID) - assert.Equal(t, "Menschen Museum", l.LocName) + assert.Equal(t, 189675302, l.PlaceID) + assert.Equal(t, "Fernsehturm Berlin", l.LocName) assert.Equal(t, "10178", l.Address.Postcode) assert.Equal(t, "Berlin", l.Address.State) assert.Equal(t, "de", l.Address.CountryCode) @@ -66,14 +69,15 @@ func TestFindLocation(t *testing.T) { t.Run("No Location", func(t *testing.T) { lat := 0.0 lng := 0.0 + id := s2.Token(lat, lng) - l, err := FindLocation(lat, lng) + l, err := FindLocation(id) if err == nil { t.Fatal("err should not be nil") } - assert.Equal(t, "osm: skipping lat 0.000000, lng 0.000000", err.Error()) + assert.Equal(t, "osm: invalid location id", err.Error()) assert.False(t, l.Cached) }) } @@ -82,7 +86,7 @@ func TestOSM_State(t *testing.T) { t.Run("Berlin", func(t *testing.T) { a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} - l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a} + l := &Location{LocCategory: "natural", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a} assert.Equal(t, "Berlin", l.State()) }) } @@ -124,7 +128,7 @@ func TestOSM_Suburb(t *testing.T) { t.Run("Neukölln", func(t *testing.T) { a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} - l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a} + l := &Location{LocCategory: "natural", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a} assert.Equal(t, "Neukölln", l.Suburb()) }) } @@ -133,34 +137,16 @@ func TestOSM_CountryCode(t *testing.T) { t.Run("de", func(t *testing.T) { a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} - l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a} + l := &Location{LocCategory: "natural", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a} assert.Equal(t, "de", l.CountryCode()) }) } -func TestOSM_Latitude(t *testing.T) { - t.Run("52.5208", func(t *testing.T) { - - a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} - l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a} - assert.Equal(t, 52.5208, l.Latitude()) - }) -} - -func TestOSM_Longitude(t *testing.T) { - t.Run("13.40953", func(t *testing.T) { - - a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} - l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a} - assert.Equal(t, 13.40953, l.Longitude()) - }) -} - func TestOSM_Keywords(t *testing.T) { t.Run("cat", func(t *testing.T) { a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} - l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "cat", Address: a} + l := &Location{LocCategory: "natural", LocName: "Nice title", LocType: "hill", LocDisplayName: "cat", Address: a} assert.Equal(t, []string{"cat"}, l.Keywords()) }) } @@ -168,6 +154,6 @@ func TestOSM_Keywords(t *testing.T) { func TestOSM_Source(t *testing.T) { a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} - l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "cat", Address: a} + l := &Location{LocCategory: "natural", LocName: "Nice title", LocType: "hill", LocDisplayName: "cat", Address: a} assert.Equal(t, "osm", l.Source()) } diff --git a/internal/maps/osm/title.go b/internal/maps/osm/title.go index 2caeec077..274256949 100644 --- a/internal/maps/osm/title.go +++ b/internal/maps/osm/title.go @@ -11,16 +11,16 @@ var labelTitles = map[string]string{ "visitor center": "Visitor Center", } -func (o Location) Name() (result string) { - result = o.Category() +func (l Location) Name() (result string) { + result = l.Category() if title, ok := labelTitles[result]; ok { - title = strings.Replace(title, "%name%", o.LocName, 1) + title = strings.Replace(title, "%name%", l.LocName, 1) return title } - if o.LocName != "" { - result = o.LocName + if l.LocName != "" { + result = l.LocName } result = strings.Replace(result, "_", " ", -1) diff --git a/internal/maps/osm/title_test.go b/internal/maps/osm/title_test.go index cae6c7933..c145b8c99 100644 --- a/internal/maps/osm/title_test.go +++ b/internal/maps/osm/title_test.go @@ -8,32 +8,32 @@ import ( func TestOSM_Name(t *testing.T) { t.Run("Nice Name", func(t *testing.T) { - l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice Name", LocType: "hill", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "natural", LocName: "Nice Name", LocType: "hill", LocDisplayName: "dipslay name"} assert.Equal(t, "Nice Name", l.Name()) }) t.Run("Water", func(t *testing.T) { - l := &Location{LocCategory: "", LocLat: "52.5208", LocLng: "13.40953", LocName: "", LocType: "water", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "", LocName: "", LocType: "water", LocDisplayName: "dipslay name"} assert.Equal(t, "Water", l.Name()) }) t.Run("Nice Name 2", func(t *testing.T) { - l := &Location{LocCategory: "shop", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice Name 2", LocType: "", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "shop", LocName: "Nice Name 2", LocType: "", LocDisplayName: "dipslay name"} assert.Equal(t, "Nice Name 2", l.Name()) }) t.Run("Cat", func(t *testing.T) { - l := &Location{LocCategory: "xxx", LocLat: "52.5208", LocLng: "13.40953", LocName: "Cat,Dog", LocType: "", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "xxx", LocName: "Cat,Dog", LocType: "", LocDisplayName: "dipslay name"} assert.Equal(t, "Cat", l.Name()) }) t.Run("airport", func(t *testing.T) { - l := &Location{LocCategory: "aeroway", LocLat: "52.5208", LocLng: "13.40953", LocName: "", LocType: "", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "aeroway", LocName: "", LocType: "", LocDisplayName: "dipslay name"} assert.Equal(t, "Airport", l.Name()) }) t.Run("Cow", func(t *testing.T) { - l := &Location{LocCategory: "xxx", LocLat: "52.5208", LocLng: "13.40953", LocName: "Cow - Cat - Dog", LocType: "", LocDisplayName: "dipslay name"} + l := &Location{LocCategory: "xxx", LocName: "Cow - Cat - Dog", LocType: "", LocDisplayName: "dipslay name"} assert.Equal(t, "Cow", l.Name()) }) } diff --git a/internal/maps/places/cache.go b/internal/maps/places/cache.go new file mode 100644 index 000000000..0b457e042 --- /dev/null +++ b/internal/maps/places/cache.go @@ -0,0 +1,9 @@ +package places + +import ( + "time" + + gc "github.com/patrickmn/go-cache" +) + +var cache = gc.New(15*time.Minute, 5*time.Minute) diff --git a/internal/maps/places/location.go b/internal/maps/places/location.go new file mode 100644 index 000000000..5fa018e82 --- /dev/null +++ b/internal/maps/places/location.go @@ -0,0 +1,121 @@ +package places + +import ( + "encoding/json" + "fmt" + "net/http" + + gc "github.com/patrickmn/go-cache" + "github.com/photoprism/photoprism/internal/s2" + "github.com/photoprism/photoprism/internal/util" +) + +// Location +type Location struct { + ID string `json:"id"` + LocLat float64 `json:"lat"` + LocLng float64 `json:"lng"` + LocName string `json:"name"` + LocCategory string `json:"category"` + LocSuburb string `json:"suburb"` + Place Place `json:"place"` + Cached bool `json:"-"` +} + +var ReverseLookupURL = "https://places.photoprism.org/v1/location/%s" + +func FindLocation(id string) (result Location, err error) { + if len(id) > 16 || len(id) == 0 { + return result, fmt.Errorf("places: invalid location id %s", id) + } + + lat, lng := s2.LatLng(id) + + if lat == 0.0 || lng == 0.0 { + return result, fmt.Errorf("places: skipping lat %f, lng %f", lat, lng) + } + + if hit, ok := cache.Get(id); ok { + log.Debugf("places: cache hit for lat %f, lng %f", lat, lng) + result = hit.(Location) + result.Cached = true + return result, nil + } + + url := fmt.Sprintf(ReverseLookupURL, id) + + log.Debugf("places: query %s", url) + + r, err := http.Get(url) + + if err != nil { + log.Errorf("places: %s", err.Error()) + return result, err + } + + err = json.NewDecoder(r.Body).Decode(&result) + + if err != nil { + log.Errorf("places: %s", err.Error()) + return result, err + } + + if result.ID == "" { + log.Debugf("result: %+v", result) + return result, fmt.Errorf("places: no result for %s", id) + } + + cache.Set(id, result, gc.DefaultExpiration) + + result.Cached = false + + return result, nil +} + +func (l Location) CellID() (result string) { + return l.ID +} + +func (l Location) Name() (result string) { + return l.LocName +} + +func (l Location) Category() (result string) { + return l.LocCategory +} + +func (l Location) Label() (result string) { + return l.Place.LocLabel +} + +func (l Location) State() (result string) { + return l.Place.LocState +} + +func (l Location) City() (result string) { + return l.Place.LocCity +} + +func (l Location) Suburb() (result string) { + return l.LocSuburb +} + +func (l Location) CountryCode() (result string) { + return l.Place.LocCountry +} + +func (l Location) Latitude() (result float64) { + return l.LocLat +} + +func (l Location) Longitude() (result float64) { + return l.LocLng +} + +func (l Location) Keywords() (result []string) { + return util.Keywords(l.Label()) +} + +func (l Location) Source() string { + return "places" +} diff --git a/internal/maps/places/location_test.go b/internal/maps/places/location_test.go new file mode 100644 index 000000000..f3c882035 --- /dev/null +++ b/internal/maps/places/location_test.go @@ -0,0 +1,27 @@ +package places + +import ( + "testing" + + "github.com/photoprism/photoprism/internal/s2" + "github.com/stretchr/testify/assert" +) + +func TestFindLocation(t *testing.T) { + t.Run("U Berliner Rathaus", func(t *testing.T) { + lat := 52.51961810676184 + lng := 13.40806264572578 + id := s2.Token(lat, lng) + + l, err := FindLocation(id) + + if err != nil { + t.Fatal(err) + } + + assert.False(t, l.Cached) + assert.Equal(t, "U Berliner Rathaus", l.Name()) + assert.Equal(t, "Berlin", l.City()) + assert.Equal(t, "de", l.CountryCode()) + }) +} diff --git a/internal/maps/places/place.go b/internal/maps/places/place.go new file mode 100644 index 000000000..ced0d6de7 --- /dev/null +++ b/internal/maps/places/place.go @@ -0,0 +1,10 @@ +package places + +// Place +type Place struct { + PlaceID string `json:"id"` + LocLabel string `json:"label"` + LocCity string `json:"city"` + LocState string `json:"state"` + LocCountry string `json:"country"` +} diff --git a/internal/maps/places/places.go b/internal/maps/places/places.go new file mode 100644 index 000000000..86b1250ee --- /dev/null +++ b/internal/maps/places/places.go @@ -0,0 +1,14 @@ +/* +This package encapsulates the PhotoPrism Places API. + +Additional information can be found in our Developer Guide: + +https://github.com/photoprism/photoprism/wiki +*/ +package places + +import ( + "github.com/photoprism/photoprism/internal/event" +) + +var log = event.Log diff --git a/internal/maps/s2.go b/internal/maps/s2.go deleted file mode 100644 index 8d19dcaaf..000000000 --- a/internal/maps/s2.go +++ /dev/null @@ -1,26 +0,0 @@ -package maps - -import ( - "github.com/golang/geo/s2" -) - -var S2Level = 15 - -func S2Token(lat, lng float64) string { - return S2TokenLevel(lat, lng, S2Level) -} - -func S2TokenLevel(lat, lng float64, level int) string { - if lat < -90 || lat > 90 { - log.Warnf("olc: latitude out of range (%f)", lat) - return "" - } - - if lng < -180 || lng > 180 { - log.Warnf("olc: longitude out of range (%f)", lng) - return "" - } - - l := s2.LatLngFromDegrees(lat, lng) - return s2.CellIDFromLatLng(l).Parent(level).ToToken() -} diff --git a/internal/maps/s2_test.go b/internal/maps/s2_test.go deleted file mode 100644 index 90712b6af..000000000 --- a/internal/maps/s2_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package maps - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestS2Token(t *testing.T) { - t.Run("Wildgehege", func(t *testing.T) { - plusCode := S2Token(48.56344833333333, 8.996878333333333) - expected := "4799e370c" - - assert.Equal(t, expected, plusCode) - }) - - t.Run("LatOverflow", func(t *testing.T) { - plusCode := S2Token(548.56344833333333, 8.996878333333333) - expected := "" - - assert.Equal(t, expected, plusCode) - }) - - t.Run("LongOverflow", func(t *testing.T) { - plusCode := S2Token(48.56344833333333, 258.996878333333333) - expected := "" - - assert.Equal(t, expected, plusCode) - }) -} - -func TestS2TokenLevel(t *testing.T) { - t.Run("Wildgehege30", func(t *testing.T) { - plusCode := S2TokenLevel(48.56344833333333, 8.996878333333333, 30) - expected := "4799e370ca54c8b9" - - assert.Equal(t, expected, plusCode) - }) - - t.Run("NearWildgehege30", func(t *testing.T) { - plusCode := S2TokenLevel(48.56344839999999, 8.996878339999999, 30) - expected := "4799e370ca54c8b7" - - assert.Equal(t, expected, plusCode) - }) - - t.Run("Wildgehege18", func(t *testing.T) { - plusCode := S2TokenLevel(48.56344833333333, 8.996878333333333, 18) - expected := "4799e370cb" - - assert.Equal(t, expected, plusCode) - }) - - t.Run("NearWildgehege18", func(t *testing.T) { - plusCode := S2TokenLevel(48.56344839999999, 8.996878339999999, 18) - expected := "4799e370cb" - - assert.Equal(t, expected, plusCode) - }) - - t.Run("NearWildgehege15", func(t *testing.T) { - plusCode := S2TokenLevel(48.56344833333333, 8.996878333333333, 15) - expected := "4799e370c" - - assert.Equal(t, expected, plusCode) - }) - - t.Run("Wildgehege10", func(t *testing.T) { - plusCode := S2TokenLevel(48.56344833333333, 8.996878333333333, 10) - expected := "4799e3" - - assert.Equal(t, expected, plusCode) - }) - - t.Run("LatOverflow", func(t *testing.T) { - plusCode := S2TokenLevel(548.56344833333333, 8.996878333333333, 30) - expected := "" - - assert.Equal(t, expected, plusCode) - }) - - t.Run("LongOverflow", func(t *testing.T) { - plusCode := S2TokenLevel(48.56344833333333, 258.996878333333333, 30) - expected := "" - - assert.Equal(t, expected, plusCode) - }) -} diff --git a/internal/photoprism/location_test.go b/internal/photoprism/location_test.go index a159543e5..8d63323e7 100644 --- a/internal/photoprism/location_test.go +++ b/internal/photoprism/location_test.go @@ -1,6 +1,7 @@ package photoprism import ( + "strings" "testing" "github.com/photoprism/photoprism/internal/config" @@ -31,8 +32,7 @@ func TestMediaFile_Location(t *testing.T) { assert.Equal(t, "Kinki Region", location.State()) assert.Equal(t, "Japan", location.CountryName()) assert.Equal(t, "", location.Category()) - assert.Equal(t, 34.79745, location.Latitude()) - assert.Equal(t, "3554df45c", location.ID) + assert.True(t, strings.HasPrefix(location.ID, "3554df45")) location2, err := mediaFile.Location() if err != nil { @@ -67,8 +67,7 @@ func TestMediaFile_Location(t *testing.T) { assert.Equal(t, "Tübingen", location.City()) assert.Equal(t, "de", location.CountryCode()) assert.Equal(t, "Germany", location.CountryName()) - assert.Equal(t, 48.53870833333333, location.Latitude()) - assert.Equal(t, "4799e4a5c", location.ID) + assert.True(t, strings.HasPrefix(location.ID, "4799e4a5")) }) t.Run("dog_orange.jpg", func(t *testing.T) { conf := config.TestConfig() diff --git a/internal/s2/s2.go b/internal/s2/s2.go new file mode 100644 index 000000000..f04193e64 --- /dev/null +++ b/internal/s2/s2.go @@ -0,0 +1,44 @@ +package s2 + +import ( + gs2 "github.com/golang/geo/s2" + "github.com/photoprism/photoprism/internal/event" +) + +var log = event.Log +var Level = 18 + +func Token(lat, lng float64) string { + return TokenLevel(lat, lng, Level) +} + +func TokenLevel(lat, lng float64, level int) string { + if lat == 0.0 && lng == 0.0 { + log.Debugf("s2: no values for latitude and longitude") + return "" + } + + if lat < -90 || lat > 90 { + log.Warnf("s2: latitude out of range (%f)", lat) + return "" + } + + if lng < -180 || lng > 180 { + log.Warnf("s2: longitude out of range (%f)", lng) + return "" + } + + l := gs2.LatLngFromDegrees(lat, lng) + return gs2.CellIDFromLatLng(l).Parent(level).ToToken() +} + +func LatLng(token string) (lat, lng float64) { + if token == "" || token == "-" { + log.Warn("s2: empty token") + return 0.0, 0.0 + } + + c := gs2.CellIDFromToken(token) + l := c.LatLng() + return l.Lat.Degrees(), l.Lng.Degrees() +} diff --git a/internal/s2/s2_test.go b/internal/s2/s2_test.go new file mode 100644 index 000000000..09c3d3bf7 --- /dev/null +++ b/internal/s2/s2_test.go @@ -0,0 +1,89 @@ +package s2 + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestToken(t *testing.T) { + t.Run("Wildgehege", func(t *testing.T) { + token := Token(48.56344833333333, 8.996878333333333) + expected := "4799e370" + + assert.True(t, strings.HasPrefix(token, expected)) + }) + + t.Run("LatOverflow", func(t *testing.T) { + token := Token(548.56344833333333, 8.996878333333333) + expected := "" + + assert.Equal(t, expected, token) + }) + + t.Run("LongOverflow", func(t *testing.T) { + token := Token(48.56344833333333, 258.996878333333333) + expected := "" + + assert.Equal(t, expected, token) + }) +} + +func TestTokenLevel(t *testing.T) { + t.Run("Wildgehege30", func(t *testing.T) { + token := TokenLevel(48.56344833333333, 8.996878333333333, 30) + expected := "4799e370ca54c8b9" + + assert.Equal(t, expected, token) + }) + + t.Run("NearWildgehege30", func(t *testing.T) { + plusCode := TokenLevel(48.56344839999999, 8.996878339999999, 30) + expected := "4799e370ca54c8b7" + + assert.Equal(t, expected, plusCode) + }) + + t.Run("Wildgehege18", func(t *testing.T) { + token := TokenLevel(48.56344833333333, 8.996878333333333, 18) + expected := "4799e370cb" + + assert.Equal(t, expected, token) + }) + + t.Run("NearWildgehege18", func(t *testing.T) { + token := TokenLevel(48.56344839999999, 8.996878339999999, 18) + expected := "4799e370cb" + + assert.Equal(t, expected, token) + }) + + t.Run("NearWildgehege15", func(t *testing.T) { + plusCode := TokenLevel(48.56344833333333, 8.996878333333333, 15) + expected := "4799e370c" + + assert.Equal(t, expected, plusCode) + }) + + t.Run("Wildgehege10", func(t *testing.T) { + token := TokenLevel(48.56344833333333, 8.996878333333333, 10) + expected := "4799e3" + + assert.Equal(t, expected, token) + }) + + t.Run("LatOverflow", func(t *testing.T) { + token := TokenLevel(548.56344833333333, 8.996878333333333, 30) + expected := "" + + assert.Equal(t, expected, token) + }) + + t.Run("LongOverflow", func(t *testing.T) { + token := TokenLevel(48.56344833333333, 258.996878333333333, 30) + expected := "" + + assert.Equal(t, expected, token) + }) +}