package entity import ( "strings" "sync" "time" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/maps" "github.com/photoprism/photoprism/pkg/s2" "github.com/photoprism/photoprism/pkg/txt" ) var cellMutex = sync.Mutex{} // Cell represents an S2 cell with metadata and reference to a place. type Cell struct { ID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;" json:"ID" yaml:"ID"` CellName string `gorm:"type:VARCHAR(200);" json:"Name" yaml:"Name,omitempty"` CellStreet string `gorm:"type:VARCHAR(100);" json:"Street" yaml:"Street,omitempty"` CellPostcode string `gorm:"type:VARCHAR(50);" json:"Postcode" yaml:"Postcode,omitempty"` CellCategory string `gorm:"type:VARCHAR(50);" json:"Category" yaml:"Category,omitempty"` PlaceID string `gorm:"type:VARBINARY(42);default:'zz'" json:"-" yaml:"PlaceID"` Place *Place `gorm:"PRELOAD:true" json:"Place" yaml:"-"` CreatedAt time.Time `json:"CreatedAt" yaml:"-"` UpdatedAt time.Time `json:"UpdatedAt" yaml:"-"` } // TableName returns the entity table name. func (Cell) TableName() string { return "cells" } // UnknownLocation is PhotoPrism's default location. var UnknownLocation = Cell{ ID: UnknownID, Place: &UnknownPlace, PlaceID: UnknownID, CellName: "", CellStreet: "", CellPostcode: "", CellCategory: "", } // CreateUnknownLocation creates the default location if not exists. func CreateUnknownLocation() { UnknownLocation = *FirstOrCreateCell(&UnknownLocation) } // NewCell creates a location using a token extracted from coordinate func NewCell(lat, lng float32) *Cell { result := &Cell{} result.ID = s2.PrefixedToken(float64(lat), float64(lng)) return result } // Refresh updates the index by retrieving the latest data from an external API. func (m *Cell) Refresh(api string) (err error) { // Unknown? if m.Unknown() { // Skip. return nil } start := time.Now() // Initialize. l := &maps.Location{ ID: s2.NormalizeToken(m.ID), } cellMutex.Lock() defer cellMutex.Unlock() // Retrieve location details from Places API. if err = l.QueryApi(api); err != nil { return err } // Unknown location or label missing? if l.Unknown() || l.Label() == "" { // Ignore. return nil } oldPlaceID := m.PlaceID place := Place{ ID: l.PlaceID(), PlaceLabel: l.Label(), PlaceDistrict: l.District(), PlaceCity: l.City(), PlaceState: l.State(), PlaceCountry: l.CountryCode(), PlaceKeywords: l.KeywordString(), PhotoCount: 1, } m.Place = &place m.PlaceID = l.PlaceID() // Create or update place. if err = place.Save(); err != nil { log.Errorf("index: %s while saving place %s", err, place.ID) } else { log.Tracef("index: updated place %s", place.ID) } m.CellName = l.Name() m.CellStreet = l.Street() m.CellPostcode = l.Postcode() m.CellCategory = l.Category() // Update cell. err = m.Save() if err != nil { log.Errorf("index: %s while updating cell %s [%s]", err, m.ID, time.Since(start)) return err } else if oldPlaceID == m.PlaceID { log.Tracef("index: cell %s keeps place_id %s", m.ID, m.PlaceID) } else if err := UnscopedDb().Table(Photo{}.TableName()). Where("place_id = ?", oldPlaceID). UpdateColumn("place_id", m.PlaceID). Error; err != nil { log.Warnf("index: %s while changing place_id from %s to %s", err, oldPlaceID, m.PlaceID) } log.Debugf("index: updated cell %s [%s]", m.ID, time.Since(start)) return err } // Find retrieves location data from the database or an external api if not known already. func (m *Cell) Find(api string) error { start := time.Now() db := Db() if err := db.Preload("Place").First(m, "id = ?", m.ID).Error; err == nil { log.Tracef("cell: found %s", m.ID) return nil } l := &maps.Location{ ID: s2.NormalizeToken(m.ID), } cellMutex.Lock() defer cellMutex.Unlock() // Retrieve location details from Places API. if err := l.QueryApi(api); err != nil { return err } if found := FindPlace(l.PlaceID()); found != nil { m.Place = found } else { place := &Place{ ID: l.PlaceID(), PlaceLabel: l.Label(), PlaceDistrict: l.District(), PlaceCity: l.City(), PlaceState: l.State(), PlaceCountry: l.CountryCode(), PlaceKeywords: l.KeywordString(), PhotoCount: 1, } if createErr := place.Create(); createErr == nil { event.Publish("count.places", event.Data{ "count": 1, }) log.Infof("cell: added place %s [%s]", place.ID, time.Since(start)) m.Place = place } else if found := FindPlace(l.PlaceID()); found != nil { m.Place = found } else { log.Errorf("cell: %s while creating place %s", createErr, place.ID) m.Place = &UnknownPlace } } m.PlaceID = m.Place.ID m.CellName = l.Name() m.CellStreet = l.Street() m.CellPostcode = l.Postcode() m.CellCategory = l.Category() if createErr := db.Create(m).Error; createErr == nil { log.Debugf("cell: added %s [%s]", m.ID, time.Since(start)) return nil } else if findErr := db.Preload("Place").First(m, "id = ?", m.ID).Error; findErr != nil { log.Errorf("cell: %s (create %s)", createErr, m.ID) log.Errorf("cell: %s (find %s)", findErr, m.ID) return createErr } else { log.Tracef("cell: found %s [%s]", m.ID, time.Since(start)) } return nil } // Create inserts a new row to the database. func (m *Cell) Create() error { return Db().Create(m).Error } // Save updates the record in the database or inserts a new record if it does not already exist. func (m *Cell) Save() error { return Db().Save(m).Error } // Delete removes the entity from the index. func (m *Cell) Delete() (err error) { return UnscopedDb().Delete(m).Error } // FirstOrCreateCell fetches an existing row, inserts a new row or nil in case of errors. func FirstOrCreateCell(m *Cell) *Cell { if m.ID == "" { log.Errorf("cell: id missing") return nil } if m.PlaceID == "" { log.Errorf("cell: place id missing (find or create %s)", m.ID) return nil } result := Cell{} if findErr := Db().Where("id = ?", m.ID).Preload("Place").First(&result).Error; findErr == nil { return &result } else if createErr := m.Create(); createErr == nil { return m } else if err := Db().Where("id = ?", m.ID).Preload("Place").First(&result).Error; err == nil { return &result } else { log.Errorf("cell: %s (find or create %s)", createErr, m.ID) } return nil } // Keywords returns search keywords for a location. func (m *Cell) Keywords() (result []string) { if m.Place == nil { log.Errorf("cell: place for %s is missing - possible bug", m.ID) return result } result = append(result, txt.Keywords(txt.ReplaceSpaces(m.District(), "-"))...) 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(), "-"))...) result = append(result, txt.Keywords(m.Name())...) result = append(result, txt.Keywords(m.Street())...) result = append(result, txt.Keywords(m.Category())...) result = append(result, txt.Words(m.Place.PlaceKeywords)...) result = txt.UniqueWords(result) return result } // Unknown checks if the location has no id func (m *Cell) Unknown() bool { return m.ID == "" || m.ID == UnknownLocation.ID } // Name returns name of location func (m *Cell) Name() string { return m.CellName } // NoName checks if the location has no name func (m *Cell) NoName() bool { return m.CellName == "" } // Street returns the street name if any. func (m *Cell) Street() string { return m.CellStreet } // NoStreet checks if the location has a street. func (m *Cell) NoStreet() bool { return m.CellStreet == "" } // Postcode returns the postcode if any. func (m *Cell) Postcode() string { return m.CellPostcode } // NoPostcode checks if the location has a postcode. func (m *Cell) NoPostcode() bool { return m.CellPostcode == "" } // Category returns the location category func (m *Cell) Category() string { return m.CellCategory } // NoCategory checks id the location has no category func (m *Cell) NoCategory() bool { return m.CellCategory == "" } // Label returns the location place label func (m *Cell) Label() string { return m.Place.Label() } // District returns the district name if any. func (m *Cell) District() string { return m.Place.District() } // City returns the location city name if any. func (m *Cell) City() string { return m.Place.City() } // LongCity checks if the city name is more than 16 char func (m *Cell) LongCity() bool { return len(m.City()) > 16 } // NoCity checks if the location has no city func (m *Cell) NoCity() bool { return m.City() == "" } // CityContains checks if the location city contains the text string func (m *Cell) CityContains(text string) bool { return strings.Contains(text, m.City()) } // State returns the location place state func (m *Cell) State() string { return m.Place.State() } // NoState checks if the location place has no state func (m *Cell) NoState() bool { return m.Place.State() == "" } // CountryCode returns the location place country code func (m *Cell) CountryCode() string { return m.Place.CountryCode() } // CountryName returns the location place country name func (m *Cell) CountryName() string { return m.Place.CountryName() }