diff --git a/go.mod b/go.mod index 788bf3fa8..2166f5a2d 100644 --- a/go.mod +++ b/go.mod @@ -32,6 +32,7 @@ require ( github.com/leandro-lugaresi/hub v1.1.0 github.com/lucasb-eyer/go-colorful v1.0.2 github.com/mattn/go-isatty v0.0.4 // indirect + github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/montanaflynn/stats v0.0.0-20181214052348-945b007cb92f // indirect @@ -50,6 +51,7 @@ require ( github.com/remyoudompheng/bigfft v0.0.0-20190512091148-babf20351dd7 // indirect github.com/satori/go.uuid v1.2.0 github.com/sevlyar/go-daemon v0.1.5 + github.com/shopspring/decimal v0.0.0-20191130220710-360f2bc03045 // indirect github.com/simplereach/timeutils v1.2.0 // indirect github.com/sirupsen/logrus v1.4.2 github.com/soheilhy/cmux v0.1.4 // indirect diff --git a/go.sum b/go.sum index 17dfc900a..4c680e39e 100644 --- a/go.sum +++ b/go.sum @@ -183,6 +183,8 @@ github.com/mattn/go-sqlite3 v1.10.0 h1:jbhqpg7tQe4SupckyijYiy0mJJ/pRyHvXf7JdWK86 github.com/mattn/go-sqlite3 v1.10.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c h1:1ErTnOL2d0OvfUABvEjGcPM8cKSLxYZpJiYS4BfQ3o4= +github.com/melihmucuk/geocache v0.0.0-20160621165317-521b336a001c/go.mod h1:CX2bLGC22DrgJTaYvKt+lOi3BACGNA60hbFXh2iWebs= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= @@ -269,6 +271,8 @@ github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sevlyar/go-daemon v0.1.5 h1:Zy/6jLbM8CfqJ4x4RPr7MJlSKt90f00kNM1D401C+Qk= github.com/sevlyar/go-daemon v0.1.5/go.mod h1:6dJpPatBT9eUwM5VCw9Bt6CdX9Tk6UWvhW3MebLDRKE= +github.com/shopspring/decimal v0.0.0-20191130220710-360f2bc03045 h1:8CnFGhoe92Izugjok8nZEGYCNovJwdRFYwrEiLtG6ZQ= +github.com/shopspring/decimal v0.0.0-20191130220710-360f2bc03045/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/simplereach/timeutils v1.2.0 h1:btgOAlu9RW6de2r2qQiONhjgxdAG7BL6je0G6J/yPnA= github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8= github.com/sirupsen/logrus v0.0.0-20170323161349-3bcb09397d6d/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= diff --git a/internal/osm/address.go b/internal/osm/address.go new file mode 100644 index 000000000..0bf375c9b --- /dev/null +++ b/internal/osm/address.go @@ -0,0 +1,14 @@ +package osm + +type Address struct { + HouseNumber string `json:"house_number"` + Road string `json:"road"` + Suburb string `json:"suburb"` + Town string `json:"town"` + City string `json:"city"` + Postcode string `json:"postcode"` + County string `json:"county"` + State string `json:"state"` + Country string `json:"country"` + CountryCode string `json:"country_code"` +} diff --git a/internal/osm/cache.go b/internal/osm/cache.go new file mode 100644 index 000000000..91d74443b --- /dev/null +++ b/internal/osm/cache.go @@ -0,0 +1,19 @@ +package osm + +import ( + "time" + + "github.com/melihmucuk/geocache" +) + +var geoCache *geocache.Cache + +func init() { + c, err := geocache.NewCache(time.Hour, 5*time.Minute, geocache.WithIn1M) + + if err != nil { + log.Panicf("osm: %s", err.Error()) + } + + geoCache = c +} diff --git a/internal/osm/location.go b/internal/osm/location.go new file mode 100644 index 000000000..427ad5a28 --- /dev/null +++ b/internal/osm/location.go @@ -0,0 +1,64 @@ +package osm + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/melihmucuk/geocache" +) + +type Location struct { + PlaceID int `json:"place_id"` + Lat string `json:"lat"` + Lon string `json:"lon"` + Name string `json:"name"` + Category string `json:"category"` + Type string `json:"type"` + DisplayName string `json:"display_name"` + Address Address `json:"address"` + Cached bool +} + +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, long float64) (result Location, err error) { + if lat == 0.0 || long == 0.0 { + return result, fmt.Errorf("osm: skipping lat %f / long %f", lat, long) + } + + point := geocache.GeoPoint{Latitude: lat, Longitude: long} + + if hit, ok := geoCache.Get(point); ok { + log.Debugf("osm: cache hit for lat %f / long %f", lat, long) + result = hit.(Location) + result.Cached = true + return result, nil + } + + url := fmt.Sprintf(ReverseLookupURL, lat, long) + + log.Debugf("osm: query %s", url) + + r, err := http.Get(url) + + if err != nil { + log.Errorf("osm: %s", err.Error()) + return result, err + } + + err = json.NewDecoder(r.Body).Decode(&result) + + if err != nil { + log.Errorf("osm: %s", err.Error()) + return result, err + } + + geoCache.Set(point, result, time.Hour) + + result.Cached = false + + return result, nil +} diff --git a/internal/osm/location_test.go b/internal/osm/location_test.go new file mode 100644 index 000000000..37366e63a --- /dev/null +++ b/internal/osm/location_test.go @@ -0,0 +1,79 @@ +package osm + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFindLocation(t *testing.T) { + t.Run("Fernsehturm Berlin", func(t *testing.T) { + lat := 52.5208 + long := 13.40953 + + l, err := FindLocation(lat, long) + + if err != nil { + t.Fatal(err) + } + + assert.False(t, l.Cached) + assert.Equal(t, 189675302, l.PlaceID) + assert.Equal(t, "Fernsehturm Berlin", l.Name) + assert.Equal(t, "10178", l.Address.Postcode) + assert.Equal(t, "Berlin", l.Address.State) + assert.Equal(t, "de", l.Address.CountryCode) + assert.Equal(t, "Germany", l.Address.Country) + + l.PlaceID = 123456 + + assert.Equal(t, 123456, l.PlaceID) + + cached, err := FindLocation(lat, long) + + if err != nil { + t.Fatal(err) + } + + assert.True(t, cached.Cached) + assert.Equal(t, 189675302, cached.PlaceID) + assert.Equal(t, l.Name, cached.Name) + assert.Equal(t, l.Address.Postcode, cached.Address.Postcode) + assert.Equal(t, l.Address.State, cached.Address.State) + assert.Equal(t, l.Address.CountryCode, cached.Address.CountryCode) + assert.Equal(t, l.Address.Country, cached.Address.Country) + }) + + t.Run("Menschen Museum", func(t *testing.T) { + lat := 52.52057 + long := 13.40889 + + l, err := FindLocation(lat, long) + + if err != nil { + t.Fatal(err) + } + + assert.False(t, l.Cached) + assert.Equal(t, 48287001, l.PlaceID) + assert.Equal(t, "Menschen Museum", l.Name) + assert.Equal(t, "10178", l.Address.Postcode) + assert.Equal(t, "Berlin", l.Address.State) + assert.Equal(t, "de", l.Address.CountryCode) + assert.Equal(t, "Germany", l.Address.Country) + }) + + t.Run("No Location", func(t *testing.T) { + lat := 0.0 + long := 0.0 + + l, err := FindLocation(lat, long) + + if err == nil { + t.Fatal("err should not be nil") + } + + assert.Equal(t, "osm: skipping lat 0.000000 / long 0.000000", err.Error()) + assert.False(t, l.Cached) + }) +} diff --git a/internal/osm/osm.go b/internal/osm/osm.go new file mode 100644 index 000000000..fe086f244 --- /dev/null +++ b/internal/osm/osm.go @@ -0,0 +1,14 @@ +/* +This package encapsulates the OpenStreetMap API. + +Additional information can be found in our Developer Guide: + +https://github.com/photoprism/photoprism/wiki +*/ +package osm + +import ( + "github.com/photoprism/photoprism/internal/event" +) + +var log = event.Log