2019-12-31 07:16:11 +01:00
|
|
|
package places
|
|
|
|
|
|
|
|
import (
|
2020-09-06 14:48:09 +02:00
|
|
|
"crypto/sha1"
|
2019-12-31 07:16:11 +01:00
|
|
|
"encoding/json"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2020-02-04 05:18:22 +01:00
|
|
|
"strings"
|
2020-04-24 14:11:17 +02:00
|
|
|
"time"
|
2019-12-31 07:16:11 +01:00
|
|
|
|
2020-01-12 14:00:56 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/s2"
|
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2019-12-31 07:16:11 +01:00
|
|
|
)
|
|
|
|
|
2021-11-09 11:42:10 +01:00
|
|
|
// Location represents a specific geolocation identified by its S2 ID.
|
2019-12-31 07:16:11 +01:00
|
|
|
type Location struct {
|
|
|
|
ID string `json:"id"`
|
|
|
|
LocLat float64 `json:"lat"`
|
|
|
|
LocLng float64 `json:"lng"`
|
|
|
|
LocName string `json:"name"`
|
2021-11-18 00:46:34 +01:00
|
|
|
LocStreet string `json:"street"`
|
|
|
|
LocPostcode string `json:"postcode"`
|
2019-12-31 07:16:11 +01:00
|
|
|
LocCategory string `json:"category"`
|
|
|
|
Place Place `json:"place"`
|
|
|
|
Cached bool `json:"-"`
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// ApiName is the backend API name.
|
2020-12-04 23:16:22 +01:00
|
|
|
const ApiName = "places"
|
2020-05-26 09:02:19 +02:00
|
|
|
|
2020-09-06 14:48:09 +02:00
|
|
|
var Key = "f60f5b25d59c397989e3cd374f81cdd7710a4fca"
|
|
|
|
var Secret = "photoprism"
|
2021-11-20 12:29:21 +01:00
|
|
|
var UserAgent = "PhotoPrism/dev"
|
2020-12-02 00:18:56 +01:00
|
|
|
var ReverseLookupURL = "https://places.photoprism.app/v1/location/%s"
|
2021-11-20 12:29:21 +01:00
|
|
|
|
|
|
|
var Retries = 3
|
|
|
|
var RetryDelay = 33 * time.Millisecond
|
2020-09-06 14:18:40 +02:00
|
|
|
var client = &http.Client{Timeout: 60 * time.Second}
|
2019-12-31 07:16:11 +01:00
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// FindLocation retrieves location details from the backend API.
|
2019-12-31 07:16:11 +01:00
|
|
|
func FindLocation(id string) (result Location, err error) {
|
2021-12-09 01:10:15 +01:00
|
|
|
// Normalize S2 Cell ID.
|
|
|
|
id = s2.NormalizeToken(id)
|
|
|
|
|
|
|
|
// Valid?
|
|
|
|
if len(id) == 0 {
|
|
|
|
return result, fmt.Errorf("empty cell id")
|
|
|
|
} else if n := len(id); n < 4 || n > 16 {
|
|
|
|
return result, fmt.Errorf("invalid cell id %s", txt.Quote(id))
|
2019-12-31 07:16:11 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Remember start time.
|
2020-05-30 01:41:47 +02:00
|
|
|
start := time.Now()
|
2021-12-09 01:10:15 +01:00
|
|
|
|
|
|
|
// Convert S2 Cell ID to latitude and longitude.
|
2019-12-31 07:16:11 +01:00
|
|
|
lat, lng := s2.LatLng(id)
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Return if latitude and longitude are null.
|
2019-12-31 07:16:11 +01:00
|
|
|
if lat == 0.0 || lng == 0.0 {
|
2021-12-09 01:10:15 +01:00
|
|
|
return result, fmt.Errorf("skipping lat %f, lng %f", lat, lng)
|
2019-12-31 07:16:11 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Location details cached?
|
2021-01-08 13:29:01 +01:00
|
|
|
if hit, ok := cache.Get(id); ok {
|
2021-11-20 12:29:21 +01:00
|
|
|
log.Tracef("places: cache hit for lat %f, lng %f", lat, lng)
|
2021-01-08 13:29:01 +01:00
|
|
|
cached := hit.(Location)
|
|
|
|
cached.Cached = true
|
|
|
|
return cached, nil
|
2019-12-31 07:16:11 +01:00
|
|
|
}
|
|
|
|
|
2021-11-20 12:29:21 +01:00
|
|
|
// Compose request URL.
|
2020-09-06 14:48:09 +02:00
|
|
|
url := fmt.Sprintf(ReverseLookupURL, id)
|
2019-12-31 07:16:11 +01:00
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Log request URL.
|
2021-11-20 12:29:21 +01:00
|
|
|
log.Tracef("places: sending request to %s", url)
|
2019-12-31 07:16:11 +01:00
|
|
|
|
2021-11-20 12:29:21 +01:00
|
|
|
// Create GET request instance.
|
2020-04-24 14:11:17 +02:00
|
|
|
req, err := http.NewRequest(http.MethodGet, url, nil)
|
2019-12-31 07:16:11 +01:00
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Ok?
|
2019-12-31 07:16:11 +01:00
|
|
|
if err != nil {
|
2020-12-04 23:16:22 +01:00
|
|
|
log.Errorf("places: %s", err.Error())
|
2019-12-31 07:16:11 +01:00
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
2021-11-20 12:29:21 +01:00
|
|
|
// Add User-Agent header?
|
|
|
|
if UserAgent != "" {
|
|
|
|
req.Header.Set("User-Agent", UserAgent)
|
|
|
|
}
|
2020-09-06 14:18:40 +02:00
|
|
|
|
2021-11-20 12:29:21 +01:00
|
|
|
// Add API key?
|
2020-09-06 14:48:09 +02:00
|
|
|
if Key != "" {
|
|
|
|
req.Header.Set("X-Key", Key)
|
|
|
|
req.Header.Set("X-Signature", fmt.Sprintf("%x", sha1.Sum([]byte(Key+url+Secret))))
|
|
|
|
}
|
|
|
|
|
2020-04-28 19:41:06 +02:00
|
|
|
var r *http.Response
|
|
|
|
|
2021-11-20 12:29:21 +01:00
|
|
|
// Perform request.
|
|
|
|
for i := 0; i < Retries; i++ {
|
2020-04-28 19:41:06 +02:00
|
|
|
r, err = client.Do(req)
|
|
|
|
|
2021-11-20 12:29:21 +01:00
|
|
|
// Successful?
|
2020-04-28 19:41:06 +02:00
|
|
|
if err == nil {
|
|
|
|
break
|
|
|
|
}
|
2021-11-20 12:29:21 +01:00
|
|
|
|
|
|
|
// Wait before trying again?
|
|
|
|
if RetryDelay.Nanoseconds() > 0 {
|
|
|
|
time.Sleep(RetryDelay)
|
|
|
|
}
|
2020-04-28 19:41:06 +02:00
|
|
|
}
|
2020-04-24 14:11:17 +02:00
|
|
|
|
2021-11-20 12:29:21 +01:00
|
|
|
// Failed?
|
2020-04-24 14:11:17 +02:00
|
|
|
if err != nil {
|
2021-12-09 01:10:15 +01:00
|
|
|
log.Errorf("places: %s (http request failed)", err.Error())
|
2020-04-24 14:11:17 +02:00
|
|
|
return result, err
|
|
|
|
} else if r.StatusCode >= 400 {
|
2021-12-09 01:10:15 +01:00
|
|
|
err = fmt.Errorf("request failed with code %d", r.StatusCode)
|
2020-04-24 14:11:17 +02:00
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
2021-11-20 12:29:21 +01:00
|
|
|
// Decode JSON response body.
|
2019-12-31 07:16:11 +01:00
|
|
|
err = json.NewDecoder(r.Body).Decode(&result)
|
|
|
|
|
|
|
|
if err != nil {
|
2021-12-09 01:10:15 +01:00
|
|
|
log.Errorf("places: %s (decode json failed)", err.Error())
|
2019-12-31 07:16:11 +01:00
|
|
|
return result, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if result.ID == "" {
|
2021-12-09 01:10:15 +01:00
|
|
|
return result, fmt.Errorf("no result for %s", id)
|
2019-12-31 07:16:11 +01:00
|
|
|
}
|
|
|
|
|
2021-01-08 13:29:01 +01:00
|
|
|
cache.SetDefault(id, result)
|
2021-12-09 01:10:15 +01:00
|
|
|
log.Tracef("places: cached cell %s [%s]", txt.Quote(id), time.Since(start))
|
2019-12-31 07:16:11 +01:00
|
|
|
|
|
|
|
result.Cached = false
|
|
|
|
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// CellID returns the S2 cell identifier string.
|
2021-11-18 00:46:34 +01:00
|
|
|
func (l Location) CellID() string {
|
2019-12-31 07:16:11 +01:00
|
|
|
return l.ID
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// PlaceID returns the place identifier string.
|
2021-11-18 00:46:34 +01:00
|
|
|
func (l Location) PlaceID() string {
|
|
|
|
return l.Place.PlaceID
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Name returns the location name if any.
|
2019-12-31 07:16:11 +01:00
|
|
|
func (l Location) Name() (result string) {
|
2020-02-04 05:18:22 +01:00
|
|
|
return strings.SplitN(l.LocName, "/", 2)[0]
|
2019-12-31 07:16:11 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Street returns the location street if any.
|
2021-11-18 00:46:34 +01:00
|
|
|
func (l Location) Street() (result string) {
|
|
|
|
return strings.SplitN(l.LocStreet, "/", 2)[0]
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Postcode returns the location postcode if any.
|
2021-11-18 00:46:34 +01:00
|
|
|
func (l Location) Postcode() (result string) {
|
|
|
|
return strings.SplitN(l.LocPostcode, "/", 2)[0]
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Category returns the location category if any.
|
2019-12-31 07:16:11 +01:00
|
|
|
func (l Location) Category() (result string) {
|
|
|
|
return l.LocCategory
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Label returns the location label.
|
2019-12-31 07:16:11 +01:00
|
|
|
func (l Location) Label() (result string) {
|
|
|
|
return l.Place.LocLabel
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// City returns the location address city name.
|
2019-12-31 07:16:11 +01:00
|
|
|
func (l Location) City() (result string) {
|
|
|
|
return l.Place.LocCity
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// District returns the location address district name.
|
2021-11-12 05:09:17 +01:00
|
|
|
func (l Location) District() (result string) {
|
|
|
|
return l.Place.LocDistrict
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// CountryCode returns the location address country code.
|
2019-12-31 07:16:11 +01:00
|
|
|
func (l Location) CountryCode() (result string) {
|
|
|
|
return l.Place.LocCountry
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// State returns the location address state name.
|
2021-11-11 16:00:42 +01:00
|
|
|
func (l Location) State() (result string) {
|
|
|
|
return txt.NormalizeState(l.Place.LocState, l.CountryCode())
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Latitude returns the location position latitude.
|
2019-12-31 07:16:11 +01:00
|
|
|
func (l Location) Latitude() (result float64) {
|
|
|
|
return l.LocLat
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Longitude returns the location position longitude.
|
2019-12-31 07:16:11 +01:00
|
|
|
func (l Location) Longitude() (result float64) {
|
|
|
|
return l.LocLng
|
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Keywords returns location keywords if any.
|
2019-12-31 07:16:11 +01:00
|
|
|
func (l Location) Keywords() (result []string) {
|
2021-05-01 11:06:44 +02:00
|
|
|
return txt.UniqueWords(txt.Words(l.Place.LocKeywords))
|
2019-12-31 07:16:11 +01:00
|
|
|
}
|
|
|
|
|
2021-12-09 01:10:15 +01:00
|
|
|
// Source returns the backend API name.
|
2019-12-31 07:16:11 +01:00
|
|
|
func (l Location) Source() string {
|
|
|
|
return "places"
|
|
|
|
}
|