2019-12-11 15:55:18 +00:00
|
|
|
package entity
|
2018-09-16 17:09:40 +00:00
|
|
|
|
2019-12-20 19:23:16 +00:00
|
|
|
import (
|
|
|
|
"strings"
|
2020-12-13 14:43:01 +00:00
|
|
|
"sync"
|
2019-12-20 19:23:16 +00:00
|
|
|
"time"
|
|
|
|
|
2020-05-26 10:46:22 +00:00
|
|
|
"github.com/photoprism/photoprism/internal/event"
|
2019-12-20 19:23:16 +00:00
|
|
|
"github.com/photoprism/photoprism/internal/maps"
|
2021-11-12 04:09:17 +00:00
|
|
|
|
2020-01-12 13:00:56 +00:00
|
|
|
"github.com/photoprism/photoprism/pkg/s2"
|
|
|
|
"github.com/photoprism/photoprism/pkg/txt"
|
2019-12-20 19:23:16 +00:00
|
|
|
)
|
2019-12-16 19:22:46 +00:00
|
|
|
|
2020-12-13 14:43:01 +00:00
|
|
|
var cellMutex = sync.Mutex{}
|
|
|
|
|
2020-07-12 06:27:05 +00:00
|
|
|
// Cell represents a S2 cell with location data.
|
|
|
|
type Cell struct {
|
2020-09-13 15:51:43 +00:00
|
|
|
ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"ID" yaml:"ID"`
|
2021-11-12 04:09:17 +00:00
|
|
|
CellName string `gorm:"type:VARCHAR(200);" json:"Name" yaml:"Name,omitempty"`
|
|
|
|
CellCategory string `gorm:"type:VARCHAR(50);" json:"Category" yaml:"Category,omitempty"`
|
2020-09-13 15:51:43 +00:00
|
|
|
PlaceID string `gorm:"type:VARBINARY(42);default:'zz'" json:"-" yaml:"PlaceID"`
|
2020-07-12 06:27:05 +00:00
|
|
|
Place *Place `gorm:"PRELOAD:true" json:"Place" yaml:"-"`
|
|
|
|
CreatedAt time.Time `json:"CreatedAt" yaml:"-"`
|
|
|
|
UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"`
|
2020-07-11 21:43:29 +00:00
|
|
|
}
|
|
|
|
|
2021-11-12 04:09:17 +00:00
|
|
|
// TableName returns the entity database table name.
|
|
|
|
func (Cell) TableName() string {
|
|
|
|
return "cells"
|
|
|
|
}
|
|
|
|
|
2020-07-12 02:48:17 +00:00
|
|
|
// UnknownLocation is PhotoPrism's default location.
|
2020-07-12 06:27:05 +00:00
|
|
|
var UnknownLocation = Cell{
|
2021-08-19 19:12:38 +00:00
|
|
|
ID: UnknownID,
|
2020-07-12 06:27:05 +00:00
|
|
|
Place: &UnknownPlace,
|
2021-08-19 19:12:38 +00:00
|
|
|
PlaceID: UnknownID,
|
2020-07-12 06:27:05 +00:00
|
|
|
CellName: "",
|
|
|
|
CellCategory: "",
|
2020-05-25 17:10:44 +00:00
|
|
|
}
|
|
|
|
|
2020-07-12 02:48:17 +00:00
|
|
|
// CreateUnknownLocation creates the default location if not exists.
|
|
|
|
func CreateUnknownLocation() {
|
2020-07-12 06:27:05 +00:00
|
|
|
FirstOrCreateCell(&UnknownLocation)
|
2019-12-20 19:23:16 +00:00
|
|
|
}
|
|
|
|
|
2020-07-12 06:27:05 +00:00
|
|
|
// NewCell creates a location using a token extracted from coordinate
|
|
|
|
func NewCell(lat, lng float32) *Cell {
|
|
|
|
result := &Cell{}
|
2019-12-20 19:23:16 +00:00
|
|
|
|
2020-06-05 14:49:32 +00:00
|
|
|
result.ID = s2.PrefixedToken(float64(lat), float64(lng))
|
2019-12-20 19:23:16 +00:00
|
|
|
|
|
|
|
return result
|
2018-09-17 16:40:57 +00:00
|
|
|
}
|
2019-12-16 19:22:46 +00:00
|
|
|
|
2021-11-12 04:09:17 +00:00
|
|
|
// Refresh updates the index by retrieving the latest data from an external API.
|
|
|
|
func (m *Cell) Refresh(api string) (err error) {
|
|
|
|
start := time.Now()
|
|
|
|
|
|
|
|
// Unknown?
|
|
|
|
if m.Unknown() {
|
|
|
|
// Skip.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Initialize.
|
|
|
|
l := &maps.Location{
|
|
|
|
ID: s2.NormalizeToken(m.ID),
|
|
|
|
}
|
|
|
|
|
|
|
|
// Query geodata API.
|
|
|
|
if err = l.QueryApi(api); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Unknown location or label missing?
|
|
|
|
if l.Unknown() || l.Label() == "" {
|
|
|
|
// Ignore.
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
place := &Place{}
|
|
|
|
|
|
|
|
// Find existing place by label.
|
|
|
|
if err := UnscopedDb().Where("place_label = ?", l.Label()).First(&place).Error; err != nil {
|
2021-11-12 05:32:58 +00:00
|
|
|
log.Tracef("places: %s for cell %s", err, m.ID)
|
2021-11-12 04:09:17 +00:00
|
|
|
place = &Place{ID: m.ID}
|
|
|
|
} else {
|
|
|
|
log.Tracef("places: found matching place %s for cell %s", place.ID, m.ID)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update place.
|
|
|
|
if res := UnscopedDb().Model(place).Updates(Values{
|
|
|
|
"PlaceLabel": l.Label(),
|
|
|
|
"PlaceCity": l.City(),
|
|
|
|
"PlaceDistrict": l.District(),
|
|
|
|
"PlaceState": l.State(),
|
|
|
|
"PlaceCountry": l.CountryCode(),
|
|
|
|
"PlaceKeywords": l.KeywordString(),
|
|
|
|
}); res.Error == nil && res.RowsAffected == 1 {
|
|
|
|
// Update cell place id, name, and category.
|
|
|
|
log.Tracef("places: updating place, name, and category for cell %s", m.ID)
|
|
|
|
m.PlaceID = place.ID
|
|
|
|
err = UnscopedDb().Model(m).Updates(Values{"PlaceID": m.PlaceID, "CellName": l.Name(), "CellCategory": l.Category()}).Error
|
|
|
|
|
|
|
|
} else {
|
|
|
|
// Update cell name and category.
|
|
|
|
log.Tracef("places: updating name and category for cell %s", m.ID)
|
|
|
|
err = UnscopedDb().Model(m).Updates(Values{"CellName": l.Name(), "CellCategory": l.Category()}).Error
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("places: refreshed cell %s [%s]", txt.Quote(m.ID), time.Since(start))
|
|
|
|
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-05-26 10:46:22 +00:00
|
|
|
// Find retrieves location data from the database or an external api if not known already.
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) Find(api string) error {
|
2020-05-25 17:10:44 +00:00
|
|
|
start := time.Now()
|
2020-04-30 18:07:03 +00:00
|
|
|
db := Db()
|
|
|
|
|
2020-05-29 10:56:24 +00:00
|
|
|
if err := db.Preload("Place").First(m, "id = ?", m.ID).Error; err == nil {
|
2020-12-04 22:16:22 +00:00
|
|
|
log.Debugf("location: found cell %s", m.ID)
|
2019-12-28 11:28:06 +00:00
|
|
|
return nil
|
2019-12-20 19:23:16 +00:00
|
|
|
}
|
|
|
|
|
2019-12-28 11:28:06 +00:00
|
|
|
l := &maps.Location{
|
2020-06-05 14:49:32 +00:00
|
|
|
ID: s2.NormalizeToken(m.ID),
|
2019-12-28 11:28:06 +00:00
|
|
|
}
|
|
|
|
|
2020-01-11 00:59:43 +00:00
|
|
|
if err := l.QueryApi(api); err != nil {
|
2019-12-20 19:23:16 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2020-07-07 14:56:02 +00:00
|
|
|
if found := FindPlace(l.PrefixedToken(), l.Label()); found != nil {
|
|
|
|
m.Place = found
|
2020-04-25 14:17:59 +00:00
|
|
|
} else {
|
2020-07-07 14:56:02 +00:00
|
|
|
place := &Place{
|
2020-07-12 06:27:05 +00:00
|
|
|
ID: l.PrefixedToken(),
|
|
|
|
PlaceLabel: l.Label(),
|
|
|
|
PlaceCity: l.City(),
|
|
|
|
PlaceState: l.State(),
|
|
|
|
PlaceCountry: l.CountryCode(),
|
|
|
|
PlaceKeywords: l.KeywordString(),
|
|
|
|
PhotoCount: 1,
|
2020-04-25 14:17:59 +00:00
|
|
|
}
|
2020-05-25 17:10:44 +00:00
|
|
|
|
2020-07-16 12:00:22 +00:00
|
|
|
if createErr := place.Create(); createErr == nil {
|
2020-05-26 10:46:22 +00:00
|
|
|
event.Publish("count.places", event.Data{
|
|
|
|
"count": 1,
|
|
|
|
})
|
|
|
|
|
2020-12-04 22:16:22 +00:00
|
|
|
log.Infof("location: added place %s [%s]", place.ID, time.Since(start))
|
2020-05-26 10:46:22 +00:00
|
|
|
|
2020-05-25 17:10:44 +00:00
|
|
|
m.Place = place
|
2020-07-07 14:56:02 +00:00
|
|
|
} else if found := FindPlace(l.PrefixedToken(), l.Label()); found != nil {
|
|
|
|
m.Place = found
|
|
|
|
} else {
|
2020-12-04 22:16:22 +00:00
|
|
|
log.Errorf("location: %s (create place %s)", createErr, place.ID)
|
2020-07-07 14:56:02 +00:00
|
|
|
m.Place = &UnknownPlace
|
2020-05-25 17:10:44 +00:00
|
|
|
}
|
2019-12-28 19:24:20 +00:00
|
|
|
}
|
|
|
|
|
2020-05-29 10:56:24 +00:00
|
|
|
m.PlaceID = m.Place.ID
|
2020-07-12 06:27:05 +00:00
|
|
|
m.CellName = l.Name()
|
|
|
|
m.CellCategory = l.Category()
|
2019-12-28 11:28:06 +00:00
|
|
|
|
2020-12-13 14:43:01 +00:00
|
|
|
cellMutex.Lock()
|
|
|
|
defer cellMutex.Unlock()
|
|
|
|
|
2020-07-16 12:00:22 +00:00
|
|
|
if createErr := db.Create(m).Error; createErr == nil {
|
2020-12-04 22:16:22 +00:00
|
|
|
log.Debugf("location: added cell %s [%s]", m.ID, time.Since(start))
|
2020-04-24 11:21:18 +00:00
|
|
|
return nil
|
2020-07-16 12:00:22 +00:00
|
|
|
} else if findErr := db.Preload("Place").First(m, "id = ?", m.ID).Error; findErr != nil {
|
2020-12-04 22:16:22 +00:00
|
|
|
log.Errorf("location: %s (create cell %s)", createErr, m.ID)
|
|
|
|
log.Errorf("location: %s (find cell %s)", findErr, m.ID)
|
2020-07-16 12:00:22 +00:00
|
|
|
return createErr
|
2020-05-25 17:10:44 +00:00
|
|
|
} else {
|
2020-12-04 22:16:22 +00:00
|
|
|
log.Debugf("location: found cell %s [%s]", m.ID, time.Since(start))
|
2020-05-25 17:10:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create inserts a new row to the database.
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) Create() error {
|
2020-05-26 09:00:39 +00:00
|
|
|
return Db().Create(m).Error
|
2019-12-20 19:23:16 +00:00
|
|
|
}
|
|
|
|
|
2020-07-12 06:27:05 +00:00
|
|
|
// FirstOrCreateCell fetches an existing row, inserts a new row or nil in case of errors.
|
|
|
|
func FirstOrCreateCell(m *Cell) *Cell {
|
2020-05-29 10:56:24 +00:00
|
|
|
if m.ID == "" {
|
2020-12-04 22:16:22 +00:00
|
|
|
log.Errorf("location: cell must not be empty")
|
2020-05-25 17:10:44 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-05-29 10:56:24 +00:00
|
|
|
if m.PlaceID == "" {
|
2020-12-14 12:31:18 +00:00
|
|
|
log.Errorf("location: place must not be empty (find or create cell %s)", m.ID)
|
2020-05-25 17:10:44 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-07-12 06:27:05 +00:00
|
|
|
result := Cell{}
|
2020-05-25 17:10:44 +00:00
|
|
|
|
2020-07-16 12:00:22 +00:00
|
|
|
if findErr := Db().Where("id = ?", m.ID).Preload("Place").First(&result).Error; findErr == nil {
|
2020-05-25 17:10:44 +00:00
|
|
|
return &result
|
2020-07-07 14:40:21 +00:00
|
|
|
} else if createErr := m.Create(); createErr == nil {
|
|
|
|
return m
|
2020-07-16 12:00:22 +00:00
|
|
|
} else if err := Db().Where("id = ?", m.ID).Preload("Place").First(&result).Error; err == nil {
|
2020-07-07 14:40:21 +00:00
|
|
|
return &result
|
|
|
|
} else {
|
2020-12-14 12:31:18 +00:00
|
|
|
log.Errorf("location: %s (find or create cell %s)", createErr, m.ID)
|
2020-05-25 17:10:44 +00:00
|
|
|
}
|
|
|
|
|
2020-07-07 14:40:21 +00:00
|
|
|
return nil
|
2020-05-25 17:10:44 +00:00
|
|
|
}
|
|
|
|
|
2020-07-07 14:40:21 +00:00
|
|
|
// Keywords returns search keywords for a location.
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) Keywords() (result []string) {
|
2020-04-28 17:41:06 +00:00
|
|
|
if m.Place == nil {
|
2020-12-04 22:16:22 +00:00
|
|
|
log.Errorf("location: place for cell %s is nil - you might have found a bug", m.ID)
|
2020-04-28 17:41:06 +00:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2020-04-16 13:57:07 +00: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 13:14:00 +00:00
|
|
|
result = append(result, txt.Keywords(m.Category())...)
|
2020-01-07 16:36:49 +00:00
|
|
|
result = append(result, txt.Keywords(m.Name())...)
|
2021-05-01 09:06:44 +00:00
|
|
|
result = append(result, txt.Words(m.Place.PlaceKeywords)...)
|
2019-12-20 19:23:16 +00:00
|
|
|
|
2020-03-25 13:14:00 +00:00
|
|
|
result = txt.UniqueWords(result)
|
|
|
|
|
2019-12-20 19:23:16 +00:00
|
|
|
return result
|
2019-12-16 19:22:46 +00:00
|
|
|
}
|
2019-12-28 11:28:06 +00:00
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// Unknown checks if the location has no id
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) Unknown() bool {
|
2020-07-12 02:48:17 +00:00
|
|
|
return m.ID == "" || m.ID == UnknownLocation.ID
|
2019-12-28 11:28:06 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// Name returns name of location
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) Name() string {
|
|
|
|
return m.CellName
|
2019-12-28 11:28:06 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// NoName checks if the location has no name
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) NoName() bool {
|
|
|
|
return m.CellName == ""
|
2019-12-28 19:24:20 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// Category returns the location category
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) Category() string {
|
|
|
|
return m.CellCategory
|
2019-12-28 11:28:06 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// NoCategory checks id the location has no category
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) NoCategory() bool {
|
|
|
|
return m.CellCategory == ""
|
2019-12-28 19:24:20 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// Label returns the location place label
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) Label() string {
|
2019-12-28 19:24:20 +00:00
|
|
|
return m.Place.Label()
|
2019-12-28 11:28:06 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// City returns the location place city
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) City() string {
|
2019-12-28 19:24:20 +00:00
|
|
|
return m.Place.City()
|
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// LongCity checks if the city name is more than 16 char
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) LongCity() bool {
|
2019-12-28 19:24:20 +00:00
|
|
|
return len(m.City()) > 16
|
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// NoCity checks if the location has no city
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) NoCity() bool {
|
2019-12-28 19:24:20 +00:00
|
|
|
return m.City() == ""
|
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// CityContains checks if the location city contains the text string
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) CityContains(text string) bool {
|
2019-12-28 19:24:20 +00:00
|
|
|
return strings.Contains(text, m.City())
|
2019-12-28 11:28:06 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// State returns the location place state
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) State() string {
|
2019-12-28 19:24:20 +00:00
|
|
|
return m.Place.State()
|
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// NoState checks if the location place has no state
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) NoState() bool {
|
2019-12-28 19:24:20 +00:00
|
|
|
return m.Place.State() == ""
|
2019-12-28 11:28:06 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// CountryCode returns the location place country code
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) CountryCode() string {
|
2019-12-28 19:24:20 +00:00
|
|
|
return m.Place.CountryCode()
|
2019-12-28 11:28:06 +00:00
|
|
|
}
|
|
|
|
|
2020-02-21 00:14:45 +00:00
|
|
|
// CountryName returns the location place country name
|
2020-07-12 06:27:05 +00:00
|
|
|
func (m *Cell) CountryName() string {
|
2019-12-28 19:24:20 +00:00
|
|
|
return m.Place.CountryName()
|
|
|
|
}
|