Metadata: More accurate location estimates #1668
This commit is contained in:
parent
097d768709
commit
87831c0a94
|
@ -13,7 +13,7 @@ func (m *Photo) TrustedTime() bool {
|
|||
return false
|
||||
} else if m.TakenAt.IsZero() || m.TakenAtLocal.IsZero() {
|
||||
return false
|
||||
} else if m.TimeZone == "" || m.TimeZoneUTC() {
|
||||
} else if m.TimeZone == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ import (
|
|||
"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() {
|
||||
|
@ -65,7 +67,7 @@ func (m *Photo) EstimateLocation(force bool) {
|
|||
|
||||
// Estimate country if taken date is unreliable.
|
||||
if SrcPriority[m.TakenSrc] <= SrcPriority[SrcName] {
|
||||
m.RemoveLocation(false)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
m.EstimateCountry()
|
||||
return
|
||||
}
|
||||
|
@ -105,80 +107,42 @@ func (m *Photo) EstimateLocation(force bool) {
|
|||
// Found?
|
||||
if len(mostRecent) == 0 {
|
||||
log.Debugf("photo: unknown position at %s", m.TakenAt)
|
||||
m.RemoveLocation(false)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
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(false)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
m.EstimateCountry()
|
||||
} else if len(mostRecent) == 1 {
|
||||
m.RemoveLocation(false)
|
||||
|
||||
m.Place = recentPhoto.Place
|
||||
m.PlaceID = recentPhoto.PlaceID
|
||||
m.PhotoCountry = recentPhoto.PhotoCountry
|
||||
m.PlaceSrc = SrcEstimate
|
||||
|
||||
m.UpdateTimeZone(recentPhoto.TimeZone)
|
||||
|
||||
log.Debugf("photo: approximate place of %s is %s (id %s)", m, txt.Quote(m.Place.Label()), recentPhoto.PlaceID)
|
||||
m.AdoptPlace(recentPhoto, SrcEstimate, false)
|
||||
} else if recentPhoto.HasPlace() {
|
||||
p1 := mostRecent[0]
|
||||
p2 := mostRecent[1]
|
||||
|
||||
movement := geo.NewMovement(p1.Position(), p2.Position())
|
||||
|
||||
if movement.Km() < 100 {
|
||||
estimate := movement.EstimatePosition(m.TakenAt)
|
||||
|
||||
if m.CellID != UnknownID && estimate.InRange(float64(m.PhotoLat), float64(m.PhotoLng), geo.Meter*50) {
|
||||
log.Debugf("photo: keeping position estimate %f, %f for %s", m.PhotoLat, m.PhotoLng, m.String())
|
||||
} else {
|
||||
estimate.Randomize(geo.Meter * 5)
|
||||
|
||||
m.PhotoLat = float32(estimate.Lat)
|
||||
m.PhotoLng = float32(estimate.Lng)
|
||||
m.PlaceSrc = SrcEstimate
|
||||
m.CellAccuracy = estimate.Accuracy
|
||||
m.SetAltitude(estimate.AltitudeInt(), SrcEstimate)
|
||||
|
||||
log.Debugf("photo: %s %s", m.String(), estimate.String())
|
||||
|
||||
m.UpdateLocation()
|
||||
|
||||
if m.Place == nil {
|
||||
log.Warnf("photo: failed updating position of %s", m)
|
||||
} else {
|
||||
log.Debugf("photo: approximate place of %s is %s (id %s)", m, txt.Quote(m.Place.Label()), m.PlaceID)
|
||||
}
|
||||
}
|
||||
// Ignore inaccurate coordinate estimates.
|
||||
if estimate := movement.EstimatePosition(m.TakenAt); movement.Km() < 100 && estimate.Accuracy < Accuracy1Km {
|
||||
m.SetPosition(estimate, SrcEstimate, false)
|
||||
} else {
|
||||
m.RemoveLocation(false)
|
||||
|
||||
m.Place = recentPhoto.Place
|
||||
m.PlaceID = recentPhoto.PlaceID
|
||||
m.PlaceSrc = SrcEstimate
|
||||
m.PhotoCountry = recentPhoto.PhotoCountry
|
||||
m.SetAltitude(movement.EstimateAltitudeInt(m.TakenAt), SrcEstimate)
|
||||
|
||||
m.UpdateTimeZone(recentPhoto.TimeZone)
|
||||
m.AdoptPlace(recentPhoto, SrcEstimate, false)
|
||||
}
|
||||
} else if recentPhoto.HasCountry() {
|
||||
m.RemoveLocation(false)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
m.PhotoCountry = recentPhoto.PhotoCountry
|
||||
m.PlaceSrc = SrcEstimate
|
||||
m.UpdateTimeZone(recentPhoto.TimeZone)
|
||||
|
||||
log.Debugf("photo: probable country for %s is %s", m, txt.Quote(m.CountryName()))
|
||||
} else {
|
||||
m.RemoveLocation(false)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
m.EstimateCountry()
|
||||
}
|
||||
} else {
|
||||
log.Warnf("photo: %s has no location, uid %s", recentPhoto.PhotoName, recentPhoto.PhotoUID)
|
||||
m.RemoveLocation(false)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
m.EstimateCountry()
|
||||
}
|
||||
|
||||
|
|
|
@ -538,7 +538,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "America/Mexico_City",
|
||||
Place: PlaceFixtures.Pointer("mexico"),
|
||||
PlaceID: PlaceFixtures.Pointer("mexico").ID,
|
||||
PlaceSrc: SrcManual,
|
||||
|
@ -597,7 +597,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "America/Mexico_City",
|
||||
Place: PlaceFixtures.Pointer("mexico"),
|
||||
PlaceID: PlaceFixtures.Pointer("mexico").ID,
|
||||
PlaceSrc: SrcMeta,
|
||||
|
@ -605,8 +605,8 @@ var PhotoFixtures = PhotoMap{
|
|||
CellID: CellFixtures.Pointer("mexico").ID,
|
||||
CellAccuracy: 0,
|
||||
PhotoAltitude: 0,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoLat: 19.681944,
|
||||
PhotoLng: -98.846590,
|
||||
PhotoCountry: PlaceFixtures.Pointer("mexico").CountryCode(),
|
||||
PhotoYear: 2016,
|
||||
PhotoMonth: 11,
|
||||
|
@ -819,7 +819,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoUID: "pt9jtdre2lvl0y20",
|
||||
TakenAt: time.Date(2016, 06, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenAtLocal: time.Date(2016, 06, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenSrc: "",
|
||||
TakenSrc: SrcMeta,
|
||||
PhotoType: "image",
|
||||
TypeSrc: "",
|
||||
PhotoTitle: "Title",
|
||||
|
@ -833,7 +833,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "America/Mexico_City",
|
||||
Place: PlaceFixtures.Pointer("veryLongLocName"),
|
||||
PlaceID: PlaceFixtures.Pointer("veryLongLocName").ID,
|
||||
PlaceSrc: SrcMeta,
|
||||
|
@ -841,8 +841,8 @@ var PhotoFixtures = PhotoMap{
|
|||
CellID: CellFixtures.Pointer("veryLongLocName").ID,
|
||||
CellAccuracy: 0,
|
||||
PhotoAltitude: 0,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoLat: 19.681944,
|
||||
PhotoLng: -98.846590,
|
||||
PhotoCountry: PlaceFixtures.Pointer("veryLongLocName").CountryCode(),
|
||||
PhotoYear: 2016,
|
||||
PhotoMonth: 6,
|
||||
|
@ -878,7 +878,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoUID: "pt9jtdre2lvl0y21",
|
||||
TakenAt: time.Date(2018, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenAtLocal: time.Date(2018, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenSrc: "",
|
||||
TakenSrc: SrcMeta,
|
||||
PhotoType: "image",
|
||||
TypeSrc: "",
|
||||
PhotoTitle: "Title",
|
||||
|
@ -892,7 +892,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoPrivate: false,
|
||||
PhotoScan: false,
|
||||
PhotoPanorama: false,
|
||||
TimeZone: "",
|
||||
TimeZone: "America/Mexico_City",
|
||||
Place: PlaceFixtures.Pointer("mediumLongLocName"),
|
||||
PlaceID: PlaceFixtures.Pointer("mediumLongLocName").ID,
|
||||
PlaceSrc: SrcMeta,
|
||||
|
@ -900,8 +900,8 @@ var PhotoFixtures = PhotoMap{
|
|||
CellID: CellFixtures.Pointer("mediumLongLocName").ID,
|
||||
CellAccuracy: 0,
|
||||
PhotoAltitude: 0,
|
||||
PhotoLat: 0,
|
||||
PhotoLng: 0,
|
||||
PhotoLat: 19.681944,
|
||||
PhotoLng: -98.846590,
|
||||
PhotoCountry: PlaceFixtures.Pointer("mediumLongLocName").CountryCode(),
|
||||
PhotoYear: 2018,
|
||||
PhotoMonth: 11,
|
||||
|
@ -937,7 +937,7 @@ var PhotoFixtures = PhotoMap{
|
|||
PhotoUID: "pt9jtdre2lvl0y22",
|
||||
TakenAt: time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenAtLocal: time.Date(2013, 11, 11, 9, 7, 18, 0, time.UTC),
|
||||
TakenSrc: "name",
|
||||
TakenSrc: SrcName,
|
||||
PhotoType: "image",
|
||||
TypeSrc: "",
|
||||
PhotoTitle: "TitleToBeSet",
|
||||
|
|
|
@ -47,9 +47,62 @@ func (m *Photo) UnknownLocation() bool {
|
|||
return m.CellID == "" || m.CellID == UnknownLocation.ID || m.NoLatLng()
|
||||
}
|
||||
|
||||
// SetPosition sets a position estimate.
|
||||
func (m *Photo) SetPosition(pos geo.Position, source string, force bool) {
|
||||
if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force {
|
||||
return
|
||||
} else if pos.Lat == 0 && pos.Lng == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if m.CellID != UnknownID && pos.InRange(float64(m.PhotoLat), float64(m.PhotoLng), geo.Meter*50) {
|
||||
log.Debugf("photo: %s keeps position %f, %f", m.String(), m.PhotoLat, m.PhotoLng)
|
||||
} else {
|
||||
if pos.Estimate {
|
||||
pos.Randomize(geo.Meter * 5)
|
||||
}
|
||||
|
||||
m.PhotoLat = float32(pos.Lat)
|
||||
m.PhotoLng = float32(pos.Lng)
|
||||
m.PlaceSrc = source
|
||||
m.CellAccuracy = pos.Accuracy
|
||||
m.SetAltitude(pos.AltitudeInt(), source)
|
||||
|
||||
log.Debugf("photo: %s %s", m.String(), pos.String())
|
||||
|
||||
m.UpdateLocation()
|
||||
|
||||
if m.Place == nil {
|
||||
log.Warnf("photo: failed updating position of %s", m)
|
||||
} else {
|
||||
log.Debugf("photo: approximate place of %s is %s (id %s)", m, txt.Quote(m.Place.Label()), m.PlaceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AdoptPlace sets the place based on another photo.
|
||||
func (m *Photo) AdoptPlace(other Photo, source string, force bool) {
|
||||
if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force {
|
||||
return
|
||||
} else if other.Place == nil {
|
||||
return
|
||||
}
|
||||
|
||||
m.RemoveLocation(source, force)
|
||||
|
||||
m.Place = other.Place
|
||||
m.PlaceID = other.PlaceID
|
||||
m.PhotoCountry = other.PhotoCountry
|
||||
m.PlaceSrc = source
|
||||
|
||||
m.UpdateTimeZone(other.TimeZone)
|
||||
|
||||
log.Debugf("photo: %s now located at %s (id %s)", m.String(), txt.Quote(m.Place.Label()), m.PlaceID)
|
||||
}
|
||||
|
||||
// RemoveLocation removes the current location.
|
||||
func (m *Photo) RemoveLocation(force bool) {
|
||||
if SrcPriority[m.PlaceSrc] > SrcPriority[SrcEstimate] && !force {
|
||||
func (m *Photo) RemoveLocation(source string, force bool) {
|
||||
if SrcPriority[m.PlaceSrc] > SrcPriority[source] && !force {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,104 @@
|
|||
package entity
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/geo"
|
||||
)
|
||||
|
||||
func TestPhoto_SetPosition(t *testing.T) {
|
||||
t.Run("SrcAuto", func(t *testing.T) {
|
||||
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcAuto}
|
||||
pos := geo.Position{Lat: 1, Lng: -1, Estimate: true}
|
||||
assert.Nil(t, p.Place)
|
||||
assert.Equal(t, "", p.PlaceID)
|
||||
assert.Equal(t, "s2:479a03fda123", p.CellID)
|
||||
assert.Equal(t, 0, int(p.PhotoLat))
|
||||
assert.Equal(t, 0, int(p.PhotoLng))
|
||||
p.SetPosition(pos, SrcEstimate, false)
|
||||
assert.Equal(t, "North Atlantic Ocean", p.Place.Label())
|
||||
assert.Equal(t, "zz:NjeJTM9IXJSv", p.PlaceID)
|
||||
assert.True(t, strings.HasPrefix(p.CellID, "s2:0ffebb"))
|
||||
assert.InEpsilon(t, 1, p.PhotoLat, 0.01)
|
||||
assert.InEpsilon(t, -1, p.PhotoLng, 0.01)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_AdoptPlace(t *testing.T) {
|
||||
t.Run("SrcAuto", func(t *testing.T) {
|
||||
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: -1, PhotoLng: 1, PlaceSrc: SrcAuto}
|
||||
o := Photo{ID: 1, Place: &UnknownPlace, PlaceID: UnknownPlace.ID, CellID: "s2:479a03fda18c", PhotoLat: 15, PhotoLng: -11, PlaceSrc: SrcManual}
|
||||
assert.Nil(t, p.Place)
|
||||
assert.Equal(t, "", p.PlaceID)
|
||||
assert.Equal(t, "s2:479a03fda123", p.CellID)
|
||||
assert.Equal(t, -1, int(p.PhotoLat))
|
||||
assert.Equal(t, 1, int(p.PhotoLng))
|
||||
p.AdoptPlace(o, SrcEstimate, false)
|
||||
assert.Equal(t, &UnknownPlace, p.Place)
|
||||
assert.Equal(t, UnknownPlace.ID, p.PlaceID)
|
||||
assert.Equal(t, "zz", p.CellID)
|
||||
assert.Equal(t, 0, int(p.PhotoLat))
|
||||
assert.Equal(t, 0, int(p.PhotoLng))
|
||||
})
|
||||
t.Run("SrcManual", func(t *testing.T) {
|
||||
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcManual}
|
||||
o := Photo{ID: 1, Place: &UnknownPlace, PlaceID: UnknownPlace.ID, CellID: "s2:479a03fda18c", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcManual}
|
||||
assert.Nil(t, p.Place)
|
||||
assert.Equal(t, "", p.PlaceID)
|
||||
assert.Equal(t, "s2:479a03fda123", p.CellID)
|
||||
assert.Equal(t, 0, int(p.PhotoLat))
|
||||
assert.Equal(t, 0, int(p.PhotoLng))
|
||||
p.AdoptPlace(o, SrcEstimate, false)
|
||||
assert.Nil(t, p.Place)
|
||||
assert.Equal(t, "", p.PlaceID)
|
||||
assert.Equal(t, "s2:479a03fda123", p.CellID)
|
||||
assert.Equal(t, 0, int(p.PhotoLat))
|
||||
assert.Equal(t, 0, int(p.PhotoLng))
|
||||
})
|
||||
t.Run("Force", func(t *testing.T) {
|
||||
p := Photo{ID: 1, Place: nil, PlaceID: "", CellID: "s2:479a03fda123", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcManual}
|
||||
o := Photo{ID: 1, Place: &UnknownPlace, PlaceID: UnknownPlace.ID, CellID: "s2:479a03fda18c", PhotoLat: 0, PhotoLng: 0, PlaceSrc: SrcManual}
|
||||
assert.Nil(t, p.Place)
|
||||
assert.Equal(t, "", p.PlaceID)
|
||||
assert.Equal(t, "s2:479a03fda123", p.CellID)
|
||||
assert.Equal(t, 1, int(p.PhotoLat))
|
||||
assert.Equal(t, -1, int(p.PhotoLng))
|
||||
p.AdoptPlace(o, SrcEstimate, true)
|
||||
assert.Equal(t, &UnknownPlace, p.Place)
|
||||
assert.Equal(t, UnknownPlace.ID, p.PlaceID)
|
||||
assert.Equal(t, "zz", p.CellID)
|
||||
assert.Equal(t, 0, int(p.PhotoLat))
|
||||
assert.Equal(t, 0, int(p.PhotoLng))
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_RemoveLocation(t *testing.T) {
|
||||
t.Run("SrcAuto", func(t *testing.T) {
|
||||
m := Photo{ID: 1, PlaceID: "zz:NjeJTM9IXJSv", CellID: "s2:479a03fda18c", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcAuto}
|
||||
assert.NotEmpty(t, m.CellID)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
assert.Equal(t, "zz", m.CellID)
|
||||
assert.Equal(t, "zz", m.PlaceID)
|
||||
assert.Empty(t, m.PhotoLat)
|
||||
assert.Empty(t, m.PhotoLng)
|
||||
assert.Empty(t, m.PlaceSrc)
|
||||
})
|
||||
t.Run("SrcMeta", func(t *testing.T) {
|
||||
m := Photo{ID: 1, PlaceID: "zz:NjeJTM9IXJSv", CellID: "s2:479a03fda18c", PhotoLat: 1, PhotoLng: -1, PlaceSrc: SrcMeta}
|
||||
assert.NotEmpty(t, m.CellID)
|
||||
m.RemoveLocation(SrcEstimate, false)
|
||||
assert.Equal(t, "s2:479a03fda18c", m.CellID)
|
||||
assert.Equal(t, "zz:NjeJTM9IXJSv", m.PlaceID)
|
||||
assert.NotEmpty(t, m.PhotoLat)
|
||||
assert.NotEmpty(t, m.PhotoLng)
|
||||
assert.NotEmpty(t, m.PlaceSrc)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_SetAltitude(t *testing.T) {
|
||||
t.Run("ViaSetCoordinates", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo15")
|
||||
|
@ -190,55 +282,61 @@ func TestPhoto_TrustedLocation(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPhoto_HasLocation(t *testing.T) {
|
||||
t.Run("false", func(t *testing.T) {
|
||||
t.Run("19800101_000002_D640C559", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("19800101_000002_D640C559")
|
||||
assert.False(t, m.HasLocation())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
t.Run("Photo08", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo08")
|
||||
assert.True(t, m.HasLocation())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_HasLatLng(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
t.Run("Photo01", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo01")
|
||||
assert.True(t, m.HasLatLng())
|
||||
})
|
||||
t.Run("false", func(t *testing.T) {
|
||||
t.Run("Photo09", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo09")
|
||||
assert.True(t, m.HasLatLng())
|
||||
m.PhotoLat = 0
|
||||
m.PhotoLng = 0
|
||||
assert.False(t, m.HasLatLng())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_NoLatLng(t *testing.T) {
|
||||
t.Run("false", func(t *testing.T) {
|
||||
t.Run("Photo01", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo01")
|
||||
assert.False(t, m.NoLatLng())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
t.Run("Photo09", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo09")
|
||||
assert.False(t, m.NoLatLng())
|
||||
m.PhotoLat = 0
|
||||
m.PhotoLng = 0
|
||||
assert.True(t, m.NoLatLng())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_NoPlace(t *testing.T) {
|
||||
t.Run("true", func(t *testing.T) {
|
||||
t.Run("19800101_000002_D640C559", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("19800101_000002_D640C559")
|
||||
assert.True(t, m.UnknownPlace())
|
||||
})
|
||||
t.Run("false", func(t *testing.T) {
|
||||
t.Run("Photo08", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo08")
|
||||
assert.False(t, m.UnknownPlace())
|
||||
})
|
||||
}
|
||||
|
||||
func TestPhoto_HasPlace(t *testing.T) {
|
||||
t.Run("false", func(t *testing.T) {
|
||||
t.Run("19800101_000002_D640C559", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("19800101_000002_D640C559")
|
||||
assert.False(t, m.HasPlace())
|
||||
})
|
||||
t.Run("true", func(t *testing.T) {
|
||||
t.Run("Photo08", func(t *testing.T) {
|
||||
m := PhotoFixtures.Get("Photo08")
|
||||
assert.True(t, m.HasPlace())
|
||||
})
|
||||
|
|
|
@ -60,7 +60,7 @@ func (m *Photo) UpdateTitle(labels classify.Labels) error {
|
|||
names = txt.JoinNames(people, true)
|
||||
}
|
||||
|
||||
if m.LocationLoaded() && SrcPriority[m.PlaceSrc] > SrcPriority[SrcEstimate] {
|
||||
if m.LocationLoaded() && m.TrustedLocation() {
|
||||
knownLocation = true
|
||||
loc := m.Cell
|
||||
|
||||
|
|
|
@ -92,6 +92,18 @@ func (m *Movement) Midpoint() Position {
|
|||
}
|
||||
}
|
||||
|
||||
// Closest returns the position closest in time, either start or end.
|
||||
func (m *Movement) Closest(t time.Time) Position {
|
||||
delaStart := math.Abs(m.Start.Time.Sub(t).Seconds())
|
||||
deltaEnd := math.Abs(m.End.Time.Sub(t).Seconds())
|
||||
|
||||
if delaStart > deltaEnd {
|
||||
return m.End
|
||||
} else {
|
||||
return m.Start
|
||||
}
|
||||
}
|
||||
|
||||
// Seconds returns the movement duration in seconds.
|
||||
func (m *Movement) Seconds() float64 {
|
||||
return math.Abs(m.Duration().Seconds())
|
||||
|
@ -191,25 +203,33 @@ func (m *Movement) EstimatePosition(t time.Time) Position {
|
|||
Time: t,
|
||||
Altitude: m.EstimateAltitude(t),
|
||||
Accuracy: m.EstimateAccuracy(t),
|
||||
Estimate: true,
|
||||
}
|
||||
|
||||
if !m.Realistic() {
|
||||
if m.Realistic() {
|
||||
if t.Before(m.Start.Time) || t.After(m.End.Time) {
|
||||
s = math.Copysign(math.Sqrt(math.Abs(s)), s)
|
||||
}
|
||||
|
||||
latSec, lngSec := m.DegPerSecond()
|
||||
|
||||
estimate.Lat = m.Start.Lat + latSec*s
|
||||
estimate.Lng = m.Start.Lng + lngSec*s
|
||||
|
||||
return estimate
|
||||
} else if km := m.Km(); km < 1 {
|
||||
p := m.Midpoint()
|
||||
|
||||
estimate.Lat = p.Lat
|
||||
estimate.Lng = p.Lng
|
||||
|
||||
return estimate
|
||||
} else {
|
||||
p := m.Closest(t)
|
||||
|
||||
estimate.Lat = p.Lat
|
||||
estimate.Lng = p.Lng
|
||||
|
||||
return estimate
|
||||
}
|
||||
|
||||
if t.Before(m.Start.Time) || t.After(m.End.Time) {
|
||||
s = math.Copysign(math.Sqrt(math.Abs(s)), s)
|
||||
}
|
||||
|
||||
latSec, lngSec := m.DegPerSecond()
|
||||
|
||||
estimate.Lat = m.Start.Lat + latSec*s
|
||||
estimate.Lng = m.Start.Lng + lngSec*s
|
||||
|
||||
return estimate
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ func TestMovement(t *testing.T) {
|
|||
// estimate @ 31.325956, 120.931898, 4.000000 m
|
||||
assert.InEpsilon(t, 52.008745, posEst1.Lat, 0.01)
|
||||
assert.InEpsilon(t, 16.025854, posEst1.Lng, 0.01)
|
||||
assert.True(t, posEst1.Estimate)
|
||||
|
||||
posEst2 := result.EstimatePosition(time.Date(2015, 5, 17, 18, 14, 34, 0, time.UTC))
|
||||
t.Log(posEst2.String())
|
||||
|
@ -50,6 +51,7 @@ func TestMovement(t *testing.T) {
|
|||
// 2015-05-17 18:14:34 @ 40.540746, 74.193174
|
||||
assert.InEpsilon(t, 40.540746, posEst2.Lat, 0.01)
|
||||
assert.InEpsilon(t, 74.193174, posEst2.Lng, 0.01)
|
||||
assert.True(t, posEst2.Estimate)
|
||||
|
||||
posMid := result.Midpoint()
|
||||
t.Log(posMid.String())
|
||||
|
@ -93,6 +95,7 @@ func TestMovement(t *testing.T) {
|
|||
// 2019-07-21 11:56:47 @ 48.299200, 8.930116
|
||||
assert.InEpsilon(t, 48.299200, posEst.Lat, 0.01)
|
||||
assert.InEpsilon(t, 8.930116, posEst.Lng, 0.01)
|
||||
assert.True(t, posEst.Estimate)
|
||||
|
||||
posMid := result.Midpoint()
|
||||
|
||||
|
@ -128,6 +131,7 @@ func TestMovement(t *testing.T) {
|
|||
// midpoint @ 48.301200, 8.928630
|
||||
assert.InEpsilon(t, 48.301200, posEst.Lat, 0.01)
|
||||
assert.InEpsilon(t, 8.928630, posEst.Lng, 0.01)
|
||||
assert.True(t, posEst.Estimate)
|
||||
|
||||
posMid := result.Midpoint()
|
||||
|
||||
|
@ -163,6 +167,7 @@ func TestMovement(t *testing.T) {
|
|||
// 2019-07-21 11:56:47: lat 48.299237, lng 8.929458
|
||||
assert.InEpsilon(t, 48.299237, posEst.Lat, 0.01)
|
||||
assert.InEpsilon(t, 8.929458, posEst.Lng, 0.01)
|
||||
assert.True(t, posEst.Estimate)
|
||||
|
||||
posMid := result.Midpoint()
|
||||
|
||||
|
@ -172,4 +177,46 @@ func TestMovement(t *testing.T) {
|
|||
assert.InEpsilon(t, 48.299300, posMid.Lat, 0.01)
|
||||
assert.InEpsilon(t, 8.929335, posMid.Lng, 0.01)
|
||||
})
|
||||
|
||||
t.Run("NotRealistic", func(t *testing.T) {
|
||||
timeEst := time.Date(2013, time.August, 10, 00, 05, 37, 0, time.UTC)
|
||||
|
||||
time1 := time.Date(2013, time.August, 9, 17, 9, 0, 0, time.UTC)
|
||||
time2 := time.Date(2013, time.August, 9, 17, 8, 44, 0, time.UTC)
|
||||
|
||||
pos1 := Position{Name: "Pos1", Time: time1, Lat: 52.6648, Lng: 13.3387}
|
||||
pos2 := Position{Name: "Pos2", Time: time2, Lat: 48.5193, Lng: 9.04933}
|
||||
|
||||
result := NewMovement(pos1, pos2)
|
||||
|
||||
t.Log(result.String())
|
||||
|
||||
// movement from 2013-08-09 17:08:44 to 2013-08-09 17:09:00 in 16.000000 s
|
||||
// Δ lat 4.145500, Δ lng 4.289370, dist 551.290399 km, speed 124040.339691 km/h
|
||||
assert.InEpsilon(t, 16.000000, result.Seconds(), 0.01)
|
||||
assert.InEpsilon(t, 4.145500, result.DegLat(), 0.01)
|
||||
assert.InEpsilon(t, 4.289370, result.DegLng(), 0.01)
|
||||
assert.InEpsilon(t, 551.290399, result.Km(), 0.01)
|
||||
assert.InEpsilon(t, 124040.339691, result.Speed(), 0.1)
|
||||
assert.False(t, result.Realistic())
|
||||
|
||||
posEst := result.EstimatePosition(timeEst)
|
||||
|
||||
t.Log(posEst.String())
|
||||
|
||||
// estimate @ 52.664800, 13.338700, alt 0.000000 m ± 275645 m
|
||||
assert.InEpsilon(t, 52.664800, posEst.Lat, 0.01)
|
||||
assert.InEpsilon(t, 13.338700, posEst.Lng, 0.01)
|
||||
assert.InEpsilon(t, 275645, posEst.Accuracy, 0.1)
|
||||
assert.True(t, posEst.Estimate)
|
||||
|
||||
posMid := result.Midpoint()
|
||||
|
||||
t.Log(posMid.String())
|
||||
|
||||
// midpoint @ 50.592050, 11.194015, alt 0.000000 m ± 0 m
|
||||
assert.InEpsilon(t, 50.592050, posMid.Lat, 0.01)
|
||||
assert.InEpsilon(t, 11.194015, posMid.Lng, 0.01)
|
||||
assert.Equal(t, 275645, result.EstimateAccuracy(timeEst))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ type Position struct {
|
|||
Lng float64 // In degree
|
||||
Altitude float64 // In meter
|
||||
Accuracy int // In meter
|
||||
Estimate bool
|
||||
}
|
||||
|
||||
// String returns the position information as string for logging.
|
||||
|
@ -34,6 +35,11 @@ func (p Position) AltitudeInt() int {
|
|||
return int(math.Round(p.Altitude))
|
||||
}
|
||||
|
||||
// Km calculates the distance to another position in km.
|
||||
func (p Position) Km(other Position) float64 {
|
||||
return math.Abs(Km(p, other))
|
||||
}
|
||||
|
||||
// InRange tests if coordinates are within a certain range of the position.
|
||||
func (p *Position) InRange(lat, lng, r float64) bool {
|
||||
switch {
|
||||
|
|
|
@ -12,12 +12,14 @@ func TestPosition_InRange(t *testing.T) {
|
|||
assert.True(t, pos.InRange(14.2, -3.0, 1.5))
|
||||
})
|
||||
t.Run("Zero", func(t *testing.T) {
|
||||
pos := Position{Lat: 0, Lng: 0}
|
||||
pos := Position{Lat: 0, Lng: 0, Estimate: true}
|
||||
assert.False(t, pos.InRange(0.1, -0.1, 1.5))
|
||||
assert.True(t, pos.Estimate)
|
||||
})
|
||||
t.Run("False", func(t *testing.T) {
|
||||
pos := Position{Lat: 15.2, Lng: -4.0}
|
||||
assert.False(t, pos.InRange(13.2, -3.0, 1.5))
|
||||
assert.False(t, pos.Estimate)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue