2019-12-11 16:55:18 +01:00
|
|
|
package entity
|
2018-09-16 19:09:40 +02:00
|
|
|
|
2019-12-20 20:23:16 +01:00
|
|
|
import (
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2020-05-26 12:46:22 +02:00
|
|
|
"github.com/photoprism/photoprism/internal/event"
|
2019-12-20 20:23:16 +01:00
|
|
|
"github.com/photoprism/photoprism/internal/maps"
|
2020-01-12 14:00:56 +01:00
|
|
|
"github.com/photoprism/photoprism/pkg/s2"
|
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2019-12-20 20:23:16 +01:00
|
|
|
)
|
2019-12-16 20:22:46 +01:00
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Location used to associate photos to location
|
2018-09-16 19:09:40 +02:00
|
|
|
type Location struct {
|
2020-05-29 12:56:24 +02:00
|
|
|
ID string `gorm:"type:varbinary(16);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
|
|
|
|
PlaceID string `gorm:"type:varbinary(16);" json:"-" yaml:"PlaceID"`
|
|
|
|
Place *Place `gorm:"PRELOAD:true" json:"Place" yaml:"-"`
|
2020-05-25 19:10:44 +02:00
|
|
|
LocName string `gorm:"type:varchar(255);" json:"Name" yaml:"Name,omitempty"`
|
|
|
|
LocCategory string `gorm:"type:varchar(64);" json:"Category" yaml:"Category,omitempty"`
|
|
|
|
LocSource string `gorm:"type:varbinary(16);" json:"Source" yaml:"Source,omitempty"`
|
|
|
|
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
|
|
|
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
|
|
|
}
|
|
|
|
|
|
|
|
// UnknownLocation is PhotoPrism's default location.
|
|
|
|
var UnknownLocation = Location{
|
2020-05-29 12:56:24 +02:00
|
|
|
ID: "zz",
|
2020-05-25 19:10:44 +02:00
|
|
|
Place: &UnknownPlace,
|
2020-05-29 12:56:24 +02:00
|
|
|
PlaceID: "zz",
|
2020-05-25 19:10:44 +02:00
|
|
|
LocName: "",
|
|
|
|
LocCategory: "",
|
|
|
|
LocSource: SrcAuto,
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateUnknownLocation creates the default location if not exists.
|
|
|
|
func CreateUnknownLocation() {
|
|
|
|
FirstOrCreateLocation(&UnknownLocation)
|
2019-12-20 20:23:16 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// NewLocation creates a location using a token extracted from coordinate
|
2020-04-26 11:41:54 +02:00
|
|
|
func NewLocation(lat, lng float32) *Location {
|
2019-12-20 20:23:16 +01:00
|
|
|
result := &Location{}
|
|
|
|
|
2020-06-05 16:49:32 +02:00
|
|
|
result.ID = s2.PrefixedToken(float64(lat), float64(lng))
|
2019-12-20 20:23:16 +01:00
|
|
|
|
|
|
|
return result
|
2018-09-17 18:40:57 +02:00
|
|
|
}
|
2019-12-16 20:22:46 +01:00
|
|
|
|
2020-05-26 12:46:22 +02:00
|
|
|
// Find retrieves location data from the database or an external api if not known already.
|
2020-04-30 20:07:03 +02:00
|
|
|
func (m *Location) Find(api string) error {
|
2020-05-25 19:10:44 +02:00
|
|
|
start := time.Now()
|
2020-04-30 20:07:03 +02:00
|
|
|
db := Db()
|
|
|
|
|
2020-05-29 12:56:24 +02:00
|
|
|
if err := db.Preload("Place").First(m, "id = ?", m.ID).Error; err == nil {
|
|
|
|
log.Infof("location: found %s (%+v)", m.ID, m)
|
2019-12-28 12:28:06 +01:00
|
|
|
return nil
|
2019-12-20 20:23:16 +01:00
|
|
|
}
|
|
|
|
|
2019-12-28 12:28:06 +01:00
|
|
|
l := &maps.Location{
|
2020-06-05 16:49:32 +02:00
|
|
|
ID: s2.NormalizeToken(m.ID),
|
2019-12-28 12:28:06 +01:00
|
|
|
}
|
|
|
|
|
2020-01-11 01:59:43 +01:00
|
|
|
if err := l.QueryApi(api); err != nil {
|
2020-05-29 12:56:24 +02:00
|
|
|
log.Errorf("location: %s failed %s", m.ID, err)
|
2019-12-20 20:23:16 +01:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-06-05 16:49:32 +02:00
|
|
|
if place := FindPlace(l.PrefixedToken(), l.Label()); place != nil {
|
2020-04-25 16:17:59 +02:00
|
|
|
m.Place = place
|
|
|
|
} else {
|
2020-05-25 19:10:44 +02:00
|
|
|
place = &Place{
|
2020-06-05 16:49:32 +02:00
|
|
|
ID: l.PrefixedToken(),
|
2020-04-28 19:41:06 +02:00
|
|
|
LocLabel: l.Label(),
|
|
|
|
LocCity: l.City(),
|
|
|
|
LocState: l.State(),
|
|
|
|
LocCountry: l.CountryCode(),
|
|
|
|
LocKeywords: l.KeywordString(),
|
2020-04-25 16:17:59 +02:00
|
|
|
}
|
2020-05-25 19:10:44 +02:00
|
|
|
|
|
|
|
if err := place.Create(); err != nil {
|
2020-05-29 12:56:24 +02:00
|
|
|
log.Errorf("place: failed adding %s %s", place.ID, err.Error())
|
2020-05-25 19:10:44 +02:00
|
|
|
m.Place = &UnknownPlace
|
|
|
|
} else {
|
2020-05-26 12:46:22 +02:00
|
|
|
event.Publish("count.places", event.Data{
|
|
|
|
"count": 1,
|
|
|
|
})
|
|
|
|
|
2020-05-29 12:56:24 +02:00
|
|
|
log.Infof("place: added %s [%s]", place.ID, time.Since(start))
|
2020-05-26 12:46:22 +02:00
|
|
|
|
2020-05-25 19:10:44 +02:00
|
|
|
m.Place = place
|
|
|
|
}
|
2019-12-28 20:24:20 +01:00
|
|
|
}
|
|
|
|
|
2020-05-29 12:56:24 +02:00
|
|
|
m.PlaceID = m.Place.ID
|
2020-04-28 19:41:06 +02:00
|
|
|
m.LocName = l.Name()
|
|
|
|
m.LocCategory = l.Category()
|
|
|
|
m.LocSource = l.Source()
|
2019-12-28 12:28:06 +01:00
|
|
|
|
2020-04-24 13:21:18 +02:00
|
|
|
if err := db.Create(m).Error; err == nil {
|
2020-05-29 12:56:24 +02:00
|
|
|
log.Infof("location: added %s [%s]", m.ID, time.Since(start))
|
2020-04-24 13:21:18 +02:00
|
|
|
return nil
|
2020-05-29 12:56:24 +02:00
|
|
|
} else if err := db.Preload("Place").First(m, "id = ?", m.ID).Error; err != nil {
|
|
|
|
log.Errorf("location: failed adding %s %s [%s]", m.ID, err.Error(), time.Since(start))
|
2020-05-25 19:10:44 +02:00
|
|
|
return err
|
|
|
|
} else {
|
2020-05-29 12:56:24 +02:00
|
|
|
log.Infof("location: found %s after second try [%s]", m.ID, time.Since(start))
|
2020-05-25 19:10:44 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create inserts a new row to the database.
|
|
|
|
func (m *Location) Create() error {
|
2020-05-26 11:00:39 +02:00
|
|
|
return Db().Create(m).Error
|
2019-12-20 20:23:16 +01:00
|
|
|
}
|
|
|
|
|
2020-05-26 11:00:39 +02:00
|
|
|
// FirstOrCreateLocation returns the existing row, inserts a new row or nil in case of errors.
|
2020-05-25 19:10:44 +02:00
|
|
|
func FirstOrCreateLocation(m *Location) *Location {
|
2020-05-29 12:56:24 +02:00
|
|
|
if m.ID == "" {
|
|
|
|
log.Errorf("location: id must not be empty")
|
2020-05-25 19:10:44 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-05-29 12:56:24 +02:00
|
|
|
if m.PlaceID == "" {
|
|
|
|
log.Errorf("location: place_id must not be empty (id %s)", m.ID)
|
2020-05-25 19:10:44 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
result := Location{}
|
|
|
|
|
2020-05-29 12:56:24 +02:00
|
|
|
if err := Db().Where("id = ?", m.ID).First(&result).Error; err == nil {
|
2020-05-25 19:10:44 +02:00
|
|
|
return &result
|
|
|
|
} else if err := m.Create(); err != nil {
|
|
|
|
log.Errorf("location: %s", err)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return m
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Keywords computes keyword based on a Location
|
2020-03-25 14:14:00 +01:00
|
|
|
func (m *Location) Keywords() (result []string) {
|
2020-04-28 19:41:06 +02:00
|
|
|
if m.Place == nil {
|
2020-05-29 12:56:24 +02:00
|
|
|
log.Errorf("location: place for %s is nil - you might have found a bug", m.ID)
|
2020-04-28 19:41:06 +02:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2020-04-16 15:57:07 +02:00
|
|
|
result = append(result, txt.Keywords(txt.ReplaceSpaces(m.City(), "-"))...)
|
|
|
|
result = append(result, txt.Keywords(txt.ReplaceSpaces(m.State(), "-"))...)
|
|
|
|
result = append(result, txt.Keywords(txt.ReplaceSpaces(m.CountryName(), "-"))...)
|
2020-03-25 14:14:00 +01:00
|
|
|
result = append(result, txt.Keywords(m.Category())...)
|
2020-01-07 17:36:49 +01:00
|
|
|
result = append(result, txt.Keywords(m.Name())...)
|
2020-04-28 19:41:06 +02:00
|
|
|
result = append(result, txt.Keywords(m.Place.LocKeywords)...)
|
2019-12-20 20:23:16 +01:00
|
|
|
|
2020-03-25 14:14:00 +01:00
|
|
|
result = txt.UniqueWords(result)
|
|
|
|
|
2019-12-20 20:23:16 +01:00
|
|
|
return result
|
2019-12-16 20:22:46 +01:00
|
|
|
}
|
2019-12-28 12:28:06 +01:00
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Unknown checks if the location has no id
|
2019-12-28 12:28:06 +01:00
|
|
|
func (m *Location) Unknown() bool {
|
2020-05-29 12:56:24 +02:00
|
|
|
return m.ID == "" || m.ID == UnknownLocation.ID
|
2019-12-28 12:28:06 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Name returns name of location
|
2019-12-28 12:28:06 +01:00
|
|
|
func (m *Location) Name() string {
|
|
|
|
return m.LocName
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// NoName checks if the location has no name
|
2019-12-28 20:24:20 +01:00
|
|
|
func (m *Location) NoName() bool {
|
|
|
|
return m.LocName == ""
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Category returns the location category
|
2019-12-28 12:28:06 +01:00
|
|
|
func (m *Location) Category() string {
|
|
|
|
return m.LocCategory
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// NoCategory checks id the location has no category
|
2019-12-28 20:24:20 +01:00
|
|
|
func (m *Location) NoCategory() bool {
|
|
|
|
return m.LocCategory == ""
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Label returns the location place label
|
2019-12-28 20:24:20 +01:00
|
|
|
func (m *Location) Label() string {
|
|
|
|
return m.Place.Label()
|
2019-12-28 12:28:06 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// City returns the location place city
|
2019-12-28 12:28:06 +01:00
|
|
|
func (m *Location) City() string {
|
2019-12-28 20:24:20 +01:00
|
|
|
return m.Place.City()
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// LongCity checks if the city name is more than 16 char
|
2019-12-28 20:24:20 +01:00
|
|
|
func (m *Location) LongCity() bool {
|
|
|
|
return len(m.City()) > 16
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// NoCity checks if the location has no city
|
2019-12-28 20:24:20 +01:00
|
|
|
func (m *Location) NoCity() bool {
|
|
|
|
return m.City() == ""
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// CityContains checks if the location city contains the text string
|
2019-12-28 20:24:20 +01:00
|
|
|
func (m *Location) CityContains(text string) bool {
|
|
|
|
return strings.Contains(text, m.City())
|
2019-12-28 12:28:06 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// State returns the location place state
|
2019-12-28 12:28:06 +01:00
|
|
|
func (m *Location) State() string {
|
2019-12-28 20:24:20 +01:00
|
|
|
return m.Place.State()
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// NoState checks if the location place has no state
|
2019-12-28 20:24:20 +01:00
|
|
|
func (m *Location) NoState() bool {
|
|
|
|
return m.Place.State() == ""
|
2019-12-28 12:28:06 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// CountryCode returns the location place country code
|
2019-12-28 12:28:06 +01:00
|
|
|
func (m *Location) CountryCode() string {
|
2019-12-28 20:24:20 +01:00
|
|
|
return m.Place.CountryCode()
|
2019-12-28 12:28:06 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// CountryName returns the location place country name
|
2019-12-28 12:28:06 +01:00
|
|
|
func (m *Location) CountryName() string {
|
2019-12-28 20:24:20 +01:00
|
|
|
return m.Place.CountryName()
|
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Notes returns the locations place notes
|
2019-12-28 20:24:20 +01:00
|
|
|
func (m *Location) Notes() string {
|
|
|
|
return m.Place.Notes()
|
2019-12-28 12:28:06 +01:00
|
|
|
}
|
|
|
|
|
2020-02-21 01:14:45 +01:00
|
|
|
// Source returns the source of location information
|
2019-12-28 12:28:06 +01:00
|
|
|
func (m *Location) Source() string {
|
|
|
|
return m.LocSource
|
|
|
|
}
|