Metadata: More accurate location estimates #1668

This commit is contained in:
Michael Mayer 2021-11-24 16:56:57 +01:00
parent 097d768709
commit 87831c0a94
10 changed files with 279 additions and 89 deletions

View file

@ -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
}

View file

@ -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())
// Ignore inaccurate coordinate estimates.
if estimate := movement.EstimatePosition(m.TakenAt); movement.Km() < 100 && estimate.Accuracy < Accuracy1Km {
m.SetPosition(estimate, SrcEstimate, false)
} 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)
}
}
} 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()
}

View file

@ -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",

View file

@ -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
}

View file

@ -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())
})

View file

@ -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

View file

@ -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,17 +203,10 @@ func (m *Movement) EstimatePosition(t time.Time) Position {
Time: t,
Altitude: m.EstimateAltitude(t),
Accuracy: m.EstimateAccuracy(t),
Estimate: true,
}
if !m.Realistic() {
p := m.Midpoint()
estimate.Lat = p.Lat
estimate.Lng = p.Lng
return estimate
}
if m.Realistic() {
if t.Before(m.Start.Time) || t.After(m.End.Time) {
s = math.Copysign(math.Sqrt(math.Abs(s)), s)
}
@ -211,5 +216,20 @@ func (m *Movement) EstimatePosition(t time.Time) Position {
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
}
}

View file

@ -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))
})
}

View file

@ -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 {

View file

@ -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)
})
}