Skip to content

Commit

Permalink
Merge pull request #397 from jovandeginste/rs-test
Browse files Browse the repository at this point in the history
Add route segment tests
  • Loading branch information
jovandeginste authored Jan 12, 2025
2 parents f0c903e + 74ef44f commit bb1cc7f
Show file tree
Hide file tree
Showing 9 changed files with 982 additions and 24 deletions.
41 changes: 24 additions & 17 deletions pkg/database/route_segment_matching.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@ import (

// MaxDeltaMeter is the maximum distance in meters that a point can be away from
// the route segment
const MaxDeltaMeter = 20
const MaxDeltaMeter = 20.0

// MaxTotalDistancePercentage is the maximum percentage of the total distance of
// the route segment that can be exceeded by the total distance matching part of
// the route
const MaxTotalDistancePercentage = 0.02
const MaxTotalDistanceFraction = 0.9

// RouteSegmentMatch is a match between a route segment and a workout
type RouteSegmentMatch struct {
Workout *Workout
RouteSegment *RouteSegment

first, last, end MapPoint // The first and last point of the route
first, last MapPoint // The first and last point of the route
end MapPoint // The last point of the workout

RouteSegmentID uint `gorm:"primaryKey"` // The ID of the route segment
WorkoutID uint `gorm:"primaryKey"` // The ID of the workout
FirstID, LastID int // The index of the first and last point of the route
Distance float64 // The total distance of the route segment for this workout
Duration time.Duration // The total duration of the route segment for this workout
Points int // The total number of points of the route segment for this workout
}

func (rsm *RouteSegmentMatch) AverageSpeed() float64 {
Expand All @@ -37,14 +37,10 @@ func (rsm *RouteSegmentMatch) AverageSpeed() float64 {
// the first and last point of the route along the route segment
func (rs *RouteSegment) NewRouteSegmentMatch(workout *Workout, p, last int) *RouteSegmentMatch {
rsm := &RouteSegmentMatch{
RouteSegmentID: rs.ID,
WorkoutID: workout.ID,
FirstID: p,
LastID: last,

first: workout.Data.Details.Points[p],
last: workout.Data.Details.Points[last],
end: workout.Data.Details.Points[len(workout.Data.Details.Points)-1],
Workout: workout,
RouteSegment: rs,
FirstID: p,
LastID: last,
}

rsm.calculate()
Expand All @@ -62,13 +58,19 @@ func (rsm *RouteSegmentMatch) IsBetterThan(current *RouteSegmentMatch) bool {
// within MaxTotalDistancePercentage of the distance of the current route
// segment
func (rsm *RouteSegmentMatch) MatchesDistance(distance float64) bool {
return math.Abs(1-(rsm.Distance/distance)) < MaxTotalDistancePercentage
return math.Abs(rsm.Distance/distance) > MaxTotalDistanceFraction
}

// calculate will calculate the total distance and duration of the route
// segment, and the total number of points of this workout along the route
// segment
func (rsm *RouteSegmentMatch) calculate() {
rsm.RouteSegmentID = rsm.RouteSegment.ID
rsm.WorkoutID = rsm.Workout.ID
rsm.first = rsm.Workout.Data.Details.Points[rsm.FirstID]
rsm.last = rsm.Workout.Data.Details.Points[rsm.LastID]
rsm.end = rsm.Workout.Data.Details.Points[len(rsm.Workout.Data.Details.Points)-1]

if rsm.FirstID <= rsm.LastID {
rsm.Distance = rsm.last.TotalDistance - rsm.first.TotalDistance
rsm.Duration = rsm.last.TotalDuration - rsm.first.TotalDuration
Expand Down Expand Up @@ -166,12 +168,16 @@ func (rs *RouteSegment) MatchSegment(workout *Workout, start int, forward bool)

if forward {
cur++

if cur == segmentLength {
return index, true
}
} else {
cur--
}

if cur%segmentLength == 0 {
return index, true
if cur == 0 {
return index, true
}
}

if !rs.Circular && index < start {
Expand All @@ -190,7 +196,8 @@ func (rs *RouteSegment) StartingPoints(points []MapPoint) []int {
start := rs.Points[0]

for i, p := range points {
if start.DistanceTo(&p) < MaxDeltaMeter {
d := start.DistanceTo(&p)
if d < MaxDeltaMeter {
r = append(r, i)
}
}
Expand Down
108 changes: 108 additions & 0 deletions pkg/database/route_segment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package database

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRouteSegment_Parse(t *testing.T) {
{
rs, err := NewRouteSegment("", "meer.gpx", []byte(meer))
assert.NoError(t, err)
assert.NotNil(t, rs)
assert.Greater(t, rs.TotalDistance, 1800.0)
}

{
rs, err := NewRouteSegment("", "finsepiste.gpx", []byte(finsepiste))
assert.NoError(t, err)
assert.NotNil(t, rs)
assert.Greater(t, rs.TotalDistance, 900.0)
}
}

func TestRouteSegment_FindMatches(t *testing.T) {
rs, err := NewRouteSegment("", "finsepiste.gpx", []byte(finsepiste))
assert.NoError(t, err)

w1, err := NewWorkout(AnonymousUser(), WorkoutTypeAutoDetect, "", "match.gpx", []byte(track))
assert.NoError(t, err)
assert.True(t, w1.Type.IsLocation())
assert.True(t, w1.HasTracks())

w2, err := NewWorkout(AnonymousUser(), WorkoutTypeAutoDetect, "", "nomatch.gpx", []byte(GpxSample1))
assert.NoError(t, err)
assert.True(t, w2.Type.IsLocation())
assert.True(t, w2.HasTracks())

workouts := []*Workout{w1, w2}
matches := rs.FindMatches(workouts)

assert.Len(t, matches, 1)
assert.Len(t, matches[0].Workout.Data.Details.Points, 158)
}

func TestRouteSegment_StartingPoints_NoMatch(t *testing.T) {
rs, err := NewRouteSegment("", "finsepiste.gpx", []byte(finsepiste))
assert.NoError(t, err)

w, err := NewWorkout(AnonymousUser(), WorkoutTypeAutoDetect, "", "nomatch.gpx", []byte(GpxSample1))
assert.NoError(t, err)

sp := rs.StartingPoints(w.Data.Details.Points)
assert.Empty(t, sp)
}

func TestRouteSegment_StartingPoints_Match(t *testing.T) {
rs, err := NewRouteSegment("", "finsepiste.gpx", []byte(finsepiste))
assert.NoError(t, err)

w, err := NewWorkout(AnonymousUser(), WorkoutTypeAutoDetect, "", "match.gpx", []byte(track))
assert.NoError(t, err)

sp := rs.StartingPoints(w.Data.Details.Points)
assert.NotEmpty(t, sp)
assert.Greater(t, len(sp), 0)

for _, p := range sp {
assert.Less(t, rs.Points[0].DistanceTo(&w.Data.Details.Points[p]), MaxDeltaMeter)
}
}

func TestRouteSegment_StartingPoints_MatchSegment(t *testing.T) {
rs, err := NewRouteSegment("", "finsepiste.gpx", []byte(finsepiste))
assert.NoError(t, err)

w, err := NewWorkout(AnonymousUser(), WorkoutTypeAutoDetect, "", "match.gpx", []byte(track))
assert.NoError(t, err)

sp := rs.StartingPoints(w.Data.Details.Points)
assert.NotEmpty(t, sp)
assert.Greater(t, len(sp), 0)

{
last, ok := rs.MatchSegment(w, 3, true)
assert.Zero(t, last)
assert.False(t, ok)
}

{
last, ok := rs.MatchSegment(w, 4, true)
assert.NotZero(t, last)
assert.True(t, ok)
}
}

func TestRouteSegment_Match(t *testing.T) {
rs, err := NewRouteSegment("", "finsepiste.gpx", []byte(finsepiste))
assert.NoError(t, err)

w, err := NewWorkout(AnonymousUser(), WorkoutTypeAutoDetect, "", "match.gpx", []byte(track))
assert.NoError(t, err)

rsm := rs.Match(w)
assert.NotNil(t, rsm)
assert.Greater(t, rsm.Distance, 900.0)
assert.True(t, rsm.MatchesDistance(rs.TotalDistance))
}
77 changes: 77 additions & 0 deletions pkg/database/segment1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package database

//nolint:lll
const meer = `
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="RunnerMaps" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">
<metadata>
<link href="http://runnermaps.nl/route/197237">
<text>Runnermaps route</text>
</link>
<time>2020-11-01T09:21:37Z</time>
</metadata>
<trk>
<name>Rotselaar Het meer</name>
<trkseg>
<trkpt lat="50.96020" lon="4.72376"><ele>0</ele></trkpt>
<trkpt lat="50.96014" lon="4.72463"><ele>0</ele></trkpt>
<trkpt lat="50.95964" lon="4.72605"><ele>0</ele></trkpt>
<trkpt lat="50.95882" lon="4.72761"><ele>0</ele></trkpt>
<trkpt lat="50.95822" lon="4.72853"><ele>0</ele></trkpt>
<trkpt lat="50.95818" lon="4.72864"><ele>0</ele></trkpt>
<trkpt lat="50.95811" lon="4.72913"><ele>0</ele></trkpt>
<trkpt lat="50.95811" lon="4.72913"><ele>0</ele></trkpt>
<trkpt lat="50.95809" lon="4.72965"><ele>0</ele></trkpt>
<trkpt lat="50.95811" lon="4.72990"><ele>0</ele></trkpt>
<trkpt lat="50.95815" lon="4.73002"><ele>0</ele></trkpt>
<trkpt lat="50.95861" lon="4.73054"><ele>0</ele></trkpt>
<trkpt lat="50.95867" lon="4.73064"><ele>0</ele></trkpt>
<trkpt lat="50.95871" lon="4.73099"><ele>0</ele></trkpt>
<trkpt lat="50.95885" lon="4.73123"><ele>0</ele></trkpt>
<trkpt lat="50.95995" lon="4.73151"><ele>0</ele></trkpt>
<trkpt lat="50.96020" lon="4.73164"><ele>0</ele></trkpt>
<trkpt lat="50.96020" lon="4.73164"><ele>0</ele></trkpt>
<trkpt lat="50.96092" lon="4.73213"><ele>0</ele></trkpt>
<trkpt lat="50.96103" lon="4.73207"><ele>0</ele></trkpt>
<trkpt lat="50.96121" lon="4.73189"><ele>0</ele></trkpt>
<trkpt lat="50.96139" lon="4.73177"><ele>0</ele></trkpt>
<trkpt lat="50.96161" lon="4.73182"><ele>0</ele></trkpt>
<trkpt lat="50.96170" lon="4.73176"><ele>0</ele></trkpt>
<trkpt lat="50.96176" lon="4.73168"><ele>0</ele></trkpt>
<trkpt lat="50.96181" lon="4.73118"><ele>0</ele></trkpt>
<trkpt lat="50.96187" lon="4.73102"><ele>0</ele></trkpt>
<trkpt lat="50.96206" lon="4.73073"><ele>0</ele></trkpt>
<trkpt lat="50.96208" lon="4.73042"><ele>0</ele></trkpt>
<trkpt lat="50.96191" lon="4.72954"><ele>0</ele></trkpt>
<trkpt lat="50.96192" lon="4.72945"><ele>0</ele></trkpt>
<trkpt lat="50.96198" lon="4.72950"><ele>0</ele></trkpt>
<trkpt lat="50.96204" lon="4.72978"><ele>0</ele></trkpt>
<trkpt lat="50.96204" lon="4.72978"><ele>0</ele></trkpt>
<trkpt lat="50.96198" lon="4.72950"><ele>0</ele></trkpt>
<trkpt lat="50.96192" lon="4.72945"><ele>0</ele></trkpt>
<trkpt lat="50.96225" lon="4.72914"><ele>0</ele></trkpt>
<trkpt lat="50.96218" lon="4.72881"><ele>0</ele></trkpt>
<trkpt lat="50.96214" lon="4.72774"><ele>0</ele></trkpt>
<trkpt lat="50.96217" lon="4.72764"><ele>0</ele></trkpt>
<trkpt lat="50.96225" lon="4.72758"><ele>0</ele></trkpt>
<trkpt lat="50.96225" lon="4.72752"><ele>0</ele></trkpt>
<trkpt lat="50.96225" lon="4.72752"><ele>0</ele></trkpt>
<trkpt lat="50.96226" lon="4.72723"><ele>0</ele></trkpt>
<trkpt lat="50.96220" lon="4.72653"><ele>0</ele></trkpt>
<trkpt lat="50.96203" lon="4.72537"><ele>0</ele></trkpt>
<trkpt lat="50.96187" lon="4.72474"><ele>0</ele></trkpt>
<trkpt lat="50.96182" lon="4.72379"><ele>0</ele></trkpt>
<trkpt lat="50.96186" lon="4.72370"><ele>0</ele></trkpt>
<trkpt lat="50.96186" lon="4.72370"><ele>0</ele></trkpt>
<trkpt lat="50.96187" lon="4.72366"><ele>0</ele></trkpt>
<trkpt lat="50.96153" lon="4.72362"><ele>0</ele></trkpt>
<trkpt lat="50.96119" lon="4.72350"><ele>0</ele></trkpt>
<trkpt lat="50.96098" lon="4.72347"><ele>0</ele></trkpt>
<trkpt lat="50.96058" lon="4.72331"><ele>0</ele></trkpt>
<trkpt lat="50.96049" lon="4.72333"><ele>0</ele></trkpt>
<trkpt lat="50.96037" lon="4.72341"><ele>0</ele></trkpt>
<trkpt lat="50.96021" lon="4.72371"><ele>0</ele></trkpt>
</trkseg>
</trk>
</gpx>
`
75 changes: 75 additions & 0 deletions pkg/database/segment2_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package database

//nolint:lll
const finsepiste = `
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="RunnerMaps" schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd http://www.garmin.com/xmlschemas/PowerExtension/v1 http://www.garmin.com/xmlschemas/PowerExtensionv1.xsd http://www.topografix.com/GPX/gpx_style/0/2 http://www.topografix.com/GPX/gpx_style/0/2/gpx_style.xsd" xmlns:gpxtpx="http://www.garmin.com/xmlschemas/TrackPointExtension/v1" xmlns:gpxx="http://www.garmin.com/xmlschemas/GpxExtensions/v3" xmlns:gpxpx="http://www.garmin.com/xmlschemas/PowerExtension/v1" xmlns:gpx_style="http://www.topografix.com/GPX/gpx_style/0/2">
<metadata>
<name>route_86636_track</name>
<author>
<name>gpx.studio</name>
<link href="https://gpx.studio"/>
</author>
<link href="http://runnermaps.nl/route/86636">
<text>Runnermaps route</text>
</link>
<time>2016-10-24T12:59:28.000Z</time>
</metadata>
<trk>
<name>finsepiste</name>
<trkseg>
<trkpt lat="50.957865" lon="4.724102">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95805" lon="4.72462">
<ele>0</ele>
</trkpt>
<trkpt lat="50.958" lon="4.72495">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95704" lon="4.72542">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95686" lon="4.7245">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95659" lon="4.72441">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95636" lon="4.7232">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95554" lon="4.72355">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95538" lon="4.7229">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95677" lon="4.72225">
<ele>0</ele>
</trkpt>
<trkpt lat="50.956884" lon="4.722784">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95747" lon="4.72242">
<ele>0</ele>
</trkpt>
<trkpt lat="50.9576" lon="4.72307">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95801" lon="4.72294">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95819" lon="4.72384">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95787" lon="4.72395">
<ele>0</ele>
</trkpt>
<trkpt lat="50.95787" lon="4.7241">
<ele>0</ele>
</trkpt>
</trkseg>
</trk>
</gpx>
`
Loading

0 comments on commit bb1cc7f

Please sign in to comment.