package entity import ( "time" "github.com/jinzhu/gorm" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/geo" "github.com/photoprism/photoprism/pkg/txt" ) const Accuracy1Km = 1000 // EstimateCountry updates the photo with an estimated country if possible. func (m *Photo) EstimateCountry() { if SrcPriority[m.PlaceSrc] > SrcPriority[SrcEstimate] || m.HasLocation() || m.HasPlace() { // Keep existing data. return } else if m.UnknownCamera() && m.PhotoType == TypeImage { // Don't estimate if it seems to be a non-photographic image. return } // Reset country. unknown := UnknownCountry.ID countryCode := unknown // Try to guess country from photo title. if code := txt.CountryCode(m.PhotoTitle); code != unknown && m.TitleSrc != SrcAuto { countryCode = code } // Try to guess country from filename and path. if countryCode == unknown { if code := txt.CountryCode(m.PhotoName); code != unknown && !fs.IsGenerated(m.PhotoName) { countryCode = code } else if code := txt.CountryCode(m.PhotoPath); code != unknown { countryCode = code } } // Try to guess country from original filename. if countryCode == unknown && m.OriginalName != "" && !fs.IsGenerated(m.OriginalName) { if code := txt.CountryCode(m.OriginalName); code != UnknownCountry.ID { countryCode = code } } // Set new country? if countryCode != unknown { m.PhotoCountry = countryCode m.PlaceSrc = SrcEstimate m.EstimatedAt = TimePointer() log.Debugf("photo: estimated country for %s is %s", m, txt.LogParam(m.CountryName())) } } // EstimateLocation updates the photo with an estimated place and country if possible. func (m *Photo) EstimateLocation(force bool) { if SrcPriority[m.PlaceSrc] > SrcPriority[SrcEstimate] || m.HasLocation() && m.PlaceSrc == SrcAuto { // Keep existing data. return } else if force || m.EstimatedAt == nil { // Proceed. } else if hours := TimeStamp().Sub(*m.EstimatedAt) / time.Hour; hours < MetadataEstimateInterval { // Ignore if location has been estimated recently (in the last 7 days by default). return } m.EstimatedAt = TimePointer() // Don't estimate if it seems to be a non-photographic image. if m.UnknownCamera() && m.PhotoType == TypeImage { m.RemoveLocation(SrcEstimate, false) m.RemoveLocationLabels() return } // Estimate country if TakenAt is unreliable. if SrcPriority[m.TakenSrc] <= SrcPriority[SrcName] { m.RemoveLocation(SrcEstimate, false) m.RemoveLocationLabels() m.EstimateCountry() return } var err error rangeMin := m.TakenAt.Add(-1 * time.Hour * 37) rangeMax := m.TakenAt.Add(time.Hour * 37) // Find photo with location info taken at a similar time... var mostRecent Photos switch DbDialect() { case MySQL: err = UnscopedDb(). Where("photo_lat <> 0 AND photo_lng <> 0"). Where("place_src <> '' AND place_src <> ? AND place_id IS NOT NULL AND place_id <> '' AND place_id <> 'zz'", SrcEstimate). Where("taken_src <> '' AND taken_at BETWEEN CAST(? AS DATETIME) AND CAST(? AS DATETIME)", rangeMin, rangeMax). Order(gorm.Expr("ABS(TIMESTAMPDIFF(SECOND, taken_at, ?))", m.TakenAt)).Limit(2). Preload("Place").Find(&mostRecent).Error case SQLite3: err = UnscopedDb(). Where("photo_lat <> 0 AND photo_lng <> 0"). Where("place_src <> '' AND place_src <> ? AND place_id IS NOT NULL AND place_id <> '' AND place_id <> 'zz'", SrcEstimate). Where("taken_src <> '' AND taken_at BETWEEN ? AND ?", rangeMin, rangeMax). Order(gorm.Expr("ABS(JulianDay(taken_at) - JulianDay(?))", m.TakenAt)).Limit(2). Preload("Place").Find(&mostRecent).Error default: log.Warnf("photo: unsupported sql dialect %s", txt.LogParam(DbDialect())) return } if err != nil { log.Warnf("photo: %s while estimating position", err) } // Found? if len(mostRecent) == 0 { log.Debugf("photo: unknown position at %s", m.TakenAt) m.RemoveLocation(SrcEstimate, false) m.RemoveLocationLabels() m.EstimateCountry() } else if recentPhoto := mostRecent[0]; recentPhoto.HasLocation() && recentPhoto.HasPlace() { // Too much time difference? if hours := recentPhoto.TakenAt.Sub(m.TakenAt) / time.Hour; hours < -36 || hours > 36 { log.Debugf("photo: skipping %s, %d hours time difference to recent position", m, hours) m.RemoveLocation(SrcEstimate, false) m.RemoveLocationLabels() m.EstimateCountry() } else if len(mostRecent) == 1 || m.UnknownCamera() { m.AdoptPlace(recentPhoto, SrcEstimate, false) } else { p1 := mostRecent[0] p2 := mostRecent[1] movement := geo.NewMovement(p1.Position(), p2.Position()) // Ignore inaccurate coordinate estimates. if estimate := movement.EstimatePosition(m.TakenAt); movement.Km() < 100 && estimate.Accuracy < Accuracy1Km { m.SetPosition(estimate, SrcEstimate, false) } else { m.AdoptPlace(recentPhoto, SrcEstimate, false) } } } else if recentPhoto.HasCountry() { log.Debugf("photo: estimated country for %s is %s", m, txt.LogParam(m.CountryName())) m.RemoveLocation(SrcEstimate, false) m.RemoveLocationLabels() m.PhotoCountry = recentPhoto.PhotoCountry m.PlaceSrc = SrcEstimate m.UpdateTimeZone(recentPhoto.TimeZone) } else { log.Warnf("photo: %s has no location, uid %s", recentPhoto.PhotoName, recentPhoto.PhotoUID) m.RemoveLocation(SrcEstimate, false) m.RemoveLocationLabels() m.EstimateCountry() } }