Backend: Add support for new Places API #173

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer 2019-12-31 07:16:11 +01:00
parent cf909e2a4e
commit 6da8bd098a
21 changed files with 492 additions and 361 deletions

View file

@ -475,10 +475,8 @@ american alligator:
- alligator - alligator
triceratops: triceratops:
label: animal
priority: -1 priority: -1
categories:
- reptile
- animal
snake: snake:
label: snake label: snake

View file

@ -7,7 +7,7 @@ COPY . .
# Build PhotoPrism # Build PhotoPrism
RUN make dep build-js install RUN make dep build-js install
# Base base image as photoprism/development # Same base image as photoprism/development
FROM ubuntu:18.04 FROM ubuntu:18.04
# Set environment variables # Set environment variables

View file

@ -6,6 +6,7 @@ import (
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/internal/maps" "github.com/photoprism/photoprism/internal/maps"
"github.com/photoprism/photoprism/internal/s2"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
) )
@ -14,8 +15,6 @@ type Location struct {
ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;"` ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;"`
PlaceID string `gorm:"type:varbinary(16);"` PlaceID string `gorm:"type:varbinary(16);"`
Place *Place Place *Place
LocLat float64
LocLng float64
LocName string `gorm:"type:varchar(100);"` LocName string `gorm:"type:varchar(100);"`
LocCategory string `gorm:"type:varchar(50);"` LocCategory string `gorm:"type:varchar(50);"`
LocSuburb string `gorm:"type:varchar(100);"` LocSuburb string `gorm:"type:varchar(100);"`
@ -27,9 +26,7 @@ type Location struct {
func NewLocation(lat, lng float64) *Location { func NewLocation(lat, lng float64) *Location {
result := &Location{} result := &Location{}
result.ID = maps.S2Token(lat, lng) result.ID = s2.Token(lat, lng)
result.LocLat = lat
result.LocLng = lng
return result return result
} }
@ -42,11 +39,9 @@ func (m *Location) Find(db *gorm.DB) error {
l := &maps.Location{ l := &maps.Location{
ID: m.ID, ID: m.ID,
LocLat: m.LocLat,
LocLng: m.LocLng,
} }
if err := l.Query(); err != nil { if err := l.QueryPlaces(); err != nil {
return err return err
} }
@ -93,14 +88,6 @@ func (m *Location) Unknown() bool {
return m.ID == "" return m.ID == ""
} }
func (m *Location) Latitude() float64 {
return m.LocLat
}
func (m *Location) Longitude() float64 {
return m.LocLng
}
func (m *Location) Name() string { func (m *Location) Name() string {
return m.LocName return m.LocName
} }

View file

@ -5,6 +5,7 @@ import (
"strings" "strings"
"github.com/photoprism/photoprism/internal/maps/osm" "github.com/photoprism/photoprism/internal/maps/osm"
"github.com/photoprism/photoprism/internal/maps/places"
) )
/* TODO /* TODO
@ -24,8 +25,6 @@ ORDER BY loc_country, album_name, taken_year;
// Photo location // Photo location
type Location struct { type Location struct {
ID string ID string
LocLat float64
LocLng float64
LocName string LocName string
LocCategory string LocCategory string
LocSuburb string LocSuburb string
@ -37,9 +36,8 @@ type Location struct {
} }
type LocationSource interface { type LocationSource interface {
CellID() string
CountryCode() string CountryCode() string
Latitude() float64
Longitude() float64
Category() string Category() string
Name() string Name() string
City() string City() string
@ -48,47 +46,53 @@ type LocationSource interface {
Source() string Source() string
} }
func NewLocation(lat, lng float64) *Location { func NewLocation(id string) *Location {
id := S2Token(lat, lng)
result := &Location{ result := &Location{
ID: id, ID: id,
LocLat: lat,
LocLng: lng,
} }
return result return result
} }
func (l *Location) Query() error { func (l *Location) QueryPlaces() error {
o, err := osm.FindLocation(l.LocLat, l.LocLng) s, err := places.FindLocation(l.ID)
if err != nil { if err != nil {
return err 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 { func (l *Location) Assign(s LocationSource) error {
l.LocSource = s.Source() l.LocSource = s.Source()
if l.LocLat == 0 { l.ID = s.CellID()
l.LocLat = s.Latitude()
}
if l.LocLng == 0 {
l.LocLng = s.Longitude()
}
if l.Unknown() { if l.Unknown() {
l.LocCategory = "unknown" l.LocCategory = "unknown"
return errors.New("maps: unknown location") return errors.New("maps: unknown location")
} }
if l.ID == "" {
l.ID = S2Token(l.LocLat, l.LocLng)
}
l.LocName = s.Name() l.LocName = s.Name()
l.LocCity = s.City() l.LocCity = s.City()
l.LocSuburb = s.Suburb() l.LocSuburb = s.Suburb()
@ -101,11 +105,7 @@ func (l *Location) Assign(s LocationSource) error {
} }
func (l *Location) Unknown() bool { func (l *Location) Unknown() bool {
if l.LocLng == 0.0 && l.LocLat == 0.0 { return l.ID == ""
return true
}
return false
} }
func (l *Location) label() string { func (l *Location) label() string {
@ -133,14 +133,6 @@ func (l *Location) label() string {
return strings.Join(loc[:], ", ") 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 { func (l Location) Name() string {
return l.LocName return l.LocName
} }

View file

@ -1,20 +1,40 @@
package maps package maps
import ( import (
"strings"
"testing" "testing"
"github.com/photoprism/photoprism/internal/maps/osm" "github.com/photoprism/photoprism/internal/maps/osm"
"github.com/photoprism/photoprism/internal/s2"
"github.com/stretchr/testify/assert" "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) { t.Run("BerlinFernsehturm", func(t *testing.T) {
lat := 52.5208 lat := 52.5208
lng := 13.40953 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) t.Fatal(err)
} }
@ -27,8 +47,9 @@ func TestLocation_Assign(t *testing.T) {
t.Run("BerlinFernsehturm", func(t *testing.T) { t.Run("BerlinFernsehturm", func(t *testing.T) {
lat := 52.5208 lat := 52.5208
lng := 13.40953 lng := 13.40953
id := s2.Token(lat, lng)
o, err := osm.FindLocation(lat, lng) o, err := osm.FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -54,8 +75,9 @@ func TestLocation_Assign(t *testing.T) {
t.Run("SantaMonica", func(t *testing.T) { t.Run("SantaMonica", func(t *testing.T) {
lat := 34.00909444444444 lat := 34.00909444444444
lng := -118.49700833333334 lng := -118.49700833333334
id := s2.Token(lat, lng)
o, err := osm.FindLocation(lat, lng) o, err := osm.FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -82,8 +104,9 @@ func TestLocation_Assign(t *testing.T) {
t.Run("AirportZurich", func(t *testing.T) { t.Run("AirportZurich", func(t *testing.T) {
lat := 47.45401666666667 lat := 47.45401666666667
lng := 8.557494444444446 lng := 8.557494444444446
id := s2.Token(lat, lng)
o, err := osm.FindLocation(lat, lng) o, err := osm.FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -111,8 +134,9 @@ func TestLocation_Assign(t *testing.T) {
t.Run("AirportTegel", func(t *testing.T) { t.Run("AirportTegel", func(t *testing.T) {
lat := 52.559864397033024 lat := 52.559864397033024
lng := 13.28895092010498 lng := 13.28895092010498
id := s2.Token(lat, lng)
o, err := osm.FindLocation(lat, lng) o, err := osm.FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -140,8 +164,9 @@ func TestLocation_Assign(t *testing.T) {
t.Run("PinkBeach", func(t *testing.T) { t.Run("PinkBeach", func(t *testing.T) {
lat := 35.26967222222222 lat := 35.26967222222222
lng := 23.53711666666667 lng := 23.53711666666667
id := s2.Token(lat, lng)
o, err := osm.FindLocation(lat, lng) o, err := osm.FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -162,7 +187,7 @@ func TestLocation_Assign(t *testing.T) {
t.Fatal(err) 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, "Pink Beach", l.LocName)
assert.Equal(t, "Chrisoskalitissa, Crete, Greece", l.LocLabel) 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) { t.Run("NewJersey", func(t *testing.T) {
lat := 40.74290 lat := 40.74290
lng := -74.04862 lng := -74.04862
id := s2.Token(lat, lng)
o, err := osm.FindLocation(lat, lng) o, err := osm.FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -192,7 +218,7 @@ func TestLocation_Assign(t *testing.T) {
t.Fatal(err) 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, "", l.LocName)
assert.Equal(t, "Jersey City, New Jersey, USA", l.LocLabel) 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) { t.Run("SouthAfrica", func(t *testing.T) {
lat := -31.976301666666668 lat := -31.976301666666668
lng := 29.148046666666666 lng := 29.148046666666666
id := s2.Token(lat, lng)
o, err := osm.FindLocation(lat, lng) o, err := osm.FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -222,7 +249,7 @@ func TestLocation_Assign(t *testing.T) {
t.Fatal(err) 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, "R411", l.LocName)
assert.Equal(t, "Eastern Cape, South Africa", l.LocLabel) 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) { t.Run("Unknown", func(t *testing.T) {
lat := -21.976301666666668 lat := -21.976301666666668
lng := 49.148046666666666 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 { if err == nil {
t.Fatal(err) t.Fatal("expected error")
} }
assert.False(t, o.Cached) assert.False(t, o.Cached)
@ -250,16 +280,18 @@ func TestLocation_Unknown(t *testing.T) {
t.Run("true", func(t *testing.T) { t.Run("true", func(t *testing.T) {
lat := 0.0 lat := 0.0
lng := 0.0 lng := 0.0
id := s2.Token(lat, lng)
l := NewLocation(lat, lng) l := NewLocation(id)
assert.Equal(t, true, l.Unknown()) assert.Equal(t, true, l.Unknown())
}) })
t.Run("false", func(t *testing.T) { t.Run("false", func(t *testing.T) {
lat := -31.976301666666668 lat := -31.976301666666668
lng := 29.148046666666666 lng := 29.148046666666666
id := s2.Token(lat, lng)
l := NewLocation(lat, lng) l := NewLocation(id)
assert.Equal(t, false, l.Unknown()) assert.Equal(t, false, l.Unknown())
}) })
@ -269,49 +301,22 @@ func TestLocation_place(t *testing.T) {
t.Run("unknown", func(t *testing.T) { t.Run("unknown", func(t *testing.T) {
lat := 0.0 lat := 0.0
lng := 0.0 lng := 0.0
id := s2.Token(lat, lng)
l := NewLocation(lat, lng) l := NewLocation(id)
assert.Equal(t, "Unknown", l.label()) assert.Equal(t, "Unknown", l.label())
}) })
t.Run("Nürnberg, Bayern, Germany", func(t *testing.T) { t.Run("Nürnberg, Bayern, Germany", func(t *testing.T) {
lat := -31.976301666666668 l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern"}
lng := 29.148046666666666
l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern"} assert.Equal(t, "Unknown", l.label())
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())
}) })
} }
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) {
lat := -31.976301666666668 l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt"}
lng := 29.148046666666666
l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt"}
assert.Equal(t, "Christkindlesmarkt", l.Name()) assert.Equal(t, "Christkindlesmarkt", l.Name())
}) })
@ -319,10 +324,7 @@ func TestLocation_Name(t *testing.T) {
func TestLocation_City(t *testing.T) { func TestLocation_City(t *testing.T) {
t.Run("Nürnberg", func(t *testing.T) { t.Run("Nürnberg", func(t *testing.T) {
lat := -31.976301666666668 l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt"}
lng := 29.148046666666666
l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt"}
assert.Equal(t, "Nürnberg", l.City()) assert.Equal(t, "Nürnberg", l.City())
}) })
@ -330,10 +332,7 @@ func TestLocation_City(t *testing.T) {
func TestLocation_Suburb(t *testing.T) { func TestLocation_Suburb(t *testing.T) {
t.Run("Hauptmarkt", func(t *testing.T) { t.Run("Hauptmarkt", func(t *testing.T) {
lat := -31.976301666666668 l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
lng := 29.148046666666666
l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
assert.Equal(t, "Hauptmarkt", l.Suburb()) assert.Equal(t, "Hauptmarkt", l.Suburb())
}) })
@ -341,10 +340,7 @@ func TestLocation_Suburb(t *testing.T) {
func TestLocation_State(t *testing.T) { func TestLocation_State(t *testing.T) {
t.Run("Bayern", func(t *testing.T) { t.Run("Bayern", func(t *testing.T) {
lat := -31.976301666666668 l := &Location{LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
lng := 29.148046666666666
l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
assert.Equal(t, "Bayern", l.State()) assert.Equal(t, "Bayern", l.State())
}) })
@ -352,10 +348,7 @@ func TestLocation_State(t *testing.T) {
func TestLocation_Category(t *testing.T) { func TestLocation_Category(t *testing.T) {
t.Run("test", func(t *testing.T) { t.Run("test", func(t *testing.T) {
lat := -31.976301666666668 l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
lng := 29.148046666666666
l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
assert.Equal(t, "test", l.Category()) assert.Equal(t, "test", l.Category())
}) })
@ -363,10 +356,7 @@ func TestLocation_Category(t *testing.T) {
func TestLocation_Source(t *testing.T) { func TestLocation_Source(t *testing.T) {
t.Run("source", func(t *testing.T) { t.Run("source", func(t *testing.T) {
lat := -31.976301666666668 l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt", LocSource: "source"}
lng := 29.148046666666666
l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt", LocSource: "source"}
assert.Equal(t, "source", l.Source()) assert.Equal(t, "source", l.Source())
}) })
@ -374,10 +364,7 @@ func TestLocation_Source(t *testing.T) {
func TestLocation_Place(t *testing.T) { func TestLocation_Place(t *testing.T) {
t.Run("test-label", func(t *testing.T) { t.Run("test-label", func(t *testing.T) {
lat := -31.976301666666668 l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
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"}
assert.Equal(t, "test-label", l.Label()) assert.Equal(t, "test-label", l.Label())
}) })
@ -385,10 +372,7 @@ func TestLocation_Place(t *testing.T) {
func TestLocation_CountryCode(t *testing.T) { func TestLocation_CountryCode(t *testing.T) {
t.Run("de", func(t *testing.T) { t.Run("de", func(t *testing.T) {
lat := -31.976301666666668 l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
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"}
assert.Equal(t, "de", l.CountryCode()) assert.Equal(t, "de", l.CountryCode())
}) })
@ -396,10 +380,7 @@ func TestLocation_CountryCode(t *testing.T) {
func TestLocation_CountryName(t *testing.T) { func TestLocation_CountryName(t *testing.T) {
t.Run("Germany", func(t *testing.T) { t.Run("Germany", func(t *testing.T) {
lat := -31.976301666666668 l := &Location{LocCategory: "test", LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"}
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"}
assert.Equal(t, "Germany", l.CountryName()) assert.Equal(t, "Germany", l.CountryName())
}) })

View file

@ -9,25 +9,25 @@ import (
func TestOSM_Category(t *testing.T) { func TestOSM_Category(t *testing.T) {
t.Run("hill", func(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()) assert.Equal(t, "hill", l.Category())
}) })
t.Run("water", func(t *testing.T) { 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()) assert.Equal(t, "water", l.Category())
}) })
t.Run("shop", func(t *testing.T) { 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()) assert.Equal(t, "shop", l.Category())
}) })
t.Run("no label found", func(t *testing.T) { 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()) assert.Equal(t, "", l.Category())
}) })
} }

View file

@ -2,10 +2,10 @@ package osm
import "fmt" import "fmt"
func (o Location) Category() (result string) { func (l Location) Category() (result string) {
key := fmt.Sprintf("%s=%s", o.LocCategory, o.LocType) key := fmt.Sprintf("%s=%s", l.LocCategory, l.LocType)
catKey := fmt.Sprintf("%s=*", o.LocCategory) catKey := fmt.Sprintf("%s=*", l.LocCategory)
typeKey := fmt.Sprintf("*=%s", o.LocType) typeKey := fmt.Sprintf("*=%s", l.LocType)
if result, ok := osmCategories[key]; ok { if result, ok := osmCategories[key]; ok {
return result return result

View file

@ -2,20 +2,20 @@ package osm
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"strconv"
"strings" "strings"
"time" "time"
"github.com/melihmucuk/geocache" "github.com/melihmucuk/geocache"
"github.com/photoprism/photoprism/internal/s2"
"github.com/photoprism/photoprism/internal/util" "github.com/photoprism/photoprism/internal/util"
) )
type Location struct { type Location struct {
ID string `json:"-"`
PlaceID int `json:"place_id"` PlaceID int `json:"place_id"`
LocLat string `json:"lat"`
LocLng string `json:"lon"`
LocName string `json:"name"` LocName string `json:"name"`
LocCategory string `json:"category"` LocCategory string `json:"category"`
LocType string `json:"type"` 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" 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 // 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 { if lat == 0.0 || lng == 0.0 {
return result, fmt.Errorf("osm: skipping lat %f, lng %f", lat, lng) 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 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) geoCache.Set(point, result, time.Hour)
result.Cached = false result.Cached = false
@ -66,23 +80,27 @@ func FindLocation(lat, lng float64) (result Location, err error) {
return result, nil return result, nil
} }
func (o Location) State() (result string) { func (l Location) CellID() (result string) {
result = o.Address.State return l.ID
}
func (l Location) State() (result string) {
result = l.Address.State
return strings.TrimSpace(result) return strings.TrimSpace(result)
} }
func (o Location) City() (result string) { func (l Location) City() (result string) {
if o.Address.City != "" { if l.Address.City != "" {
result = o.Address.City result = l.Address.City
} else if o.Address.Town != "" { } else if l.Address.Town != "" {
result = o.Address.Town result = l.Address.Town
} else if o.Address.Village != "" { } else if l.Address.Village != "" {
result = o.Address.Village result = l.Address.Village
} else if o.Address.County != "" { } else if l.Address.County != "" {
result = o.Address.County result = l.Address.County
} else if o.Address.State != "" { } else if l.Address.State != "" {
result = o.Address.State result = l.Address.State
} }
if len([]rune(result)) > 19 { if len([]rune(result)) > 19 {
@ -92,52 +110,22 @@ func (o Location) City() (result string) {
return strings.TrimSpace(result) return strings.TrimSpace(result)
} }
func (o Location) Suburb() (result string) { func (l Location) Suburb() (result string) {
result = o.Address.Suburb result = l.Address.Suburb
return strings.TrimSpace(result) return strings.TrimSpace(result)
} }
func (o Location) CountryCode() (result string) { func (l Location) CountryCode() (result string) {
result = o.Address.CountryCode result = l.Address.CountryCode
return strings.ToLower(strings.TrimSpace(result)) return strings.ToLower(strings.TrimSpace(result))
} }
func (o Location) Latitude() (result float64) { func (l Location) Keywords() (result []string) {
if o.LocLat == "" { return util.Keywords(l.LocDisplayName)
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 (o Location) Longitude() (result float64) { func (l Location) Source() string {
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 {
return "osm" return "osm"
} }

View file

@ -3,15 +3,17 @@ package osm
import ( import (
"testing" "testing"
"github.com/photoprism/photoprism/internal/s2"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestFindLocation(t *testing.T) { func TestFindLocation(t *testing.T) {
t.Run("BerlinFernsehturm", func(t *testing.T) { t.Run("Fernsehturm Berlin 1", func(t *testing.T) {
lat := 52.5208 lat := 52.5208
lng := 13.40953 lng := 13.40953
id := s2.Token(lat, lng)
l, err := FindLocation(lat, lng) l, err := FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -29,7 +31,7 @@ func TestFindLocation(t *testing.T) {
assert.Equal(t, 123456, l.PlaceID) assert.Equal(t, 123456, l.PlaceID)
cached, err := FindLocation(lat, lng) cached, err := FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -44,19 +46,20 @@ func TestFindLocation(t *testing.T) {
assert.Equal(t, l.Address.Country, cached.Address.Country) 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 lat := 52.52057
lng := 13.40889 lng := 13.40889
id := s2.Token(lat, lng)
l, err := FindLocation(lat, lng) l, err := FindLocation(id)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
assert.False(t, l.Cached) assert.False(t, l.Cached)
assert.Equal(t, 48287001, l.PlaceID) assert.Equal(t, 189675302, l.PlaceID)
assert.Equal(t, "Menschen Museum", l.LocName) assert.Equal(t, "Fernsehturm Berlin", l.LocName)
assert.Equal(t, "10178", l.Address.Postcode) assert.Equal(t, "10178", l.Address.Postcode)
assert.Equal(t, "Berlin", l.Address.State) assert.Equal(t, "Berlin", l.Address.State)
assert.Equal(t, "de", l.Address.CountryCode) assert.Equal(t, "de", l.Address.CountryCode)
@ -66,14 +69,15 @@ func TestFindLocation(t *testing.T) {
t.Run("No Location", func(t *testing.T) { t.Run("No Location", func(t *testing.T) {
lat := 0.0 lat := 0.0
lng := 0.0 lng := 0.0
id := s2.Token(lat, lng)
l, err := FindLocation(lat, lng) l, err := FindLocation(id)
if err == nil { if err == nil {
t.Fatal("err should not be 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) assert.False(t, l.Cached)
}) })
} }
@ -82,7 +86,7 @@ func TestOSM_State(t *testing.T) {
t.Run("Berlin", func(t *testing.T) { t.Run("Berlin", func(t *testing.T) {
a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} 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()) assert.Equal(t, "Berlin", l.State())
}) })
} }
@ -124,7 +128,7 @@ func TestOSM_Suburb(t *testing.T) {
t.Run("Neukölln", func(t *testing.T) { t.Run("Neukölln", func(t *testing.T) {
a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} 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()) assert.Equal(t, "Neukölln", l.Suburb())
}) })
} }
@ -133,34 +137,16 @@ func TestOSM_CountryCode(t *testing.T) {
t.Run("de", func(t *testing.T) { t.Run("de", func(t *testing.T) {
a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} 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()) 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) { func TestOSM_Keywords(t *testing.T) {
t.Run("cat", func(t *testing.T) { t.Run("cat", func(t *testing.T) {
a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} 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()) assert.Equal(t, []string{"cat"}, l.Keywords())
}) })
} }
@ -168,6 +154,6 @@ func TestOSM_Keywords(t *testing.T) {
func TestOSM_Source(t *testing.T) { func TestOSM_Source(t *testing.T) {
a := Address{CountryCode: "de", City: "Berlin", State: "Berlin", HouseNumber: "63", Suburb: "Neukölln"} 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()) assert.Equal(t, "osm", l.Source())
} }

View file

@ -11,16 +11,16 @@ var labelTitles = map[string]string{
"visitor center": "Visitor Center", "visitor center": "Visitor Center",
} }
func (o Location) Name() (result string) { func (l Location) Name() (result string) {
result = o.Category() result = l.Category()
if title, ok := labelTitles[result]; ok { if title, ok := labelTitles[result]; ok {
title = strings.Replace(title, "%name%", o.LocName, 1) title = strings.Replace(title, "%name%", l.LocName, 1)
return title return title
} }
if o.LocName != "" { if l.LocName != "" {
result = o.LocName result = l.LocName
} }
result = strings.Replace(result, "_", " ", -1) result = strings.Replace(result, "_", " ", -1)

View file

@ -8,32 +8,32 @@ import (
func TestOSM_Name(t *testing.T) { func TestOSM_Name(t *testing.T) {
t.Run("Nice Name", func(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()) assert.Equal(t, "Nice Name", l.Name())
}) })
t.Run("Water", func(t *testing.T) { 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()) assert.Equal(t, "Water", l.Name())
}) })
t.Run("Nice Name 2", func(t *testing.T) { 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()) assert.Equal(t, "Nice Name 2", l.Name())
}) })
t.Run("Cat", func(t *testing.T) { 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()) assert.Equal(t, "Cat", l.Name())
}) })
t.Run("airport", func(t *testing.T) { 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()) assert.Equal(t, "Airport", l.Name())
}) })
t.Run("Cow", func(t *testing.T) { 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()) assert.Equal(t, "Cow", l.Name())
}) })
} }

View file

@ -0,0 +1,9 @@
package places
import (
"time"
gc "github.com/patrickmn/go-cache"
)
var cache = gc.New(15*time.Minute, 5*time.Minute)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,7 @@
package photoprism package photoprism
import ( import (
"strings"
"testing" "testing"
"github.com/photoprism/photoprism/internal/config" "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, "Kinki Region", 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.Equal(t, 34.79745, location.Latitude()) assert.True(t, strings.HasPrefix(location.ID, "3554df45"))
assert.Equal(t, "3554df45c", location.ID)
location2, err := mediaFile.Location() location2, err := mediaFile.Location()
if err != nil { if err != nil {
@ -67,8 +67,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.Equal(t, 48.53870833333333, location.Latitude()) assert.True(t, strings.HasPrefix(location.ID, "4799e4a5"))
assert.Equal(t, "4799e4a5c", location.ID)
}) })
t.Run("dog_orange.jpg", func(t *testing.T) { t.Run("dog_orange.jpg", func(t *testing.T) {
conf := config.TestConfig() conf := config.TestConfig()

44
internal/s2/s2.go Normal file
View file

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

89
internal/s2/s2_test.go Normal file
View file

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