summaryrefslogtreecommitdiff
path: root/ride
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2025-04-21 11:51:03 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2025-04-21 11:51:03 +0200
commitc47470b51075fbac1adea281f3324c1d22b6ff5f (patch)
tree33df3b64d5153ac96f3d7f9086465cf81b7c8fad /ride
parentc05cd9cb6305f15e3de985e5040142e0156a1286 (diff)
Cleanup parsing
Diffstat (limited to 'ride')
-rw-r--r--ride/.snapshots/TestRideClimbLaCride7
-rw-r--r--ride/climb.go5
-rw-r--r--ride/climb_test.go18
-rw-r--r--ride/index_test.go14
-rw-r--r--ride/ride.go98
5 files changed, 115 insertions, 27 deletions
diff --git a/ride/.snapshots/TestRideClimbLaCride b/ride/.snapshots/TestRideClimbLaCride
index 386c553..456818e 100644
--- a/ride/.snapshots/TestRideClimbLaCride
+++ b/ride/.snapshots/TestRideClimbLaCride
@@ -1,4 +1,5 @@
-([]string) (len=2) {
- (string) (len=69) "Found similar climbs to 17.1km-18.8km: 1.7km at 4.8% (38 pts - Cat 4)",
- (string) (len=47) " 17.2km-18.8km: 1.6km at 4.9% (38 pts - Cat 4)"
+([]string) (len=3) {
+ (string) (len=69) "Climb 17.1km-18.8km: 1.7km at 4.8% (38 pts - Cat 4) (5m50s, 17.0km/h)",
+ (string) (len=65) " 17.2km-18.8km: 1.6km at 4.9% (38 pts - Cat 4) (7m42s, 12.8km/h)",
+ (string) (len=70) "Climb 35.1km-40.1km: 5.1km at 4.3% (94 pts - Cat 3) (18m48s, 16.2km/h)"
}
diff --git a/ride/climb.go b/ride/climb.go
index 069a7fb..df8798d 100644
--- a/ride/climb.go
+++ b/ride/climb.go
@@ -21,6 +21,11 @@ type Climb struct {
func (climb Climb) Duration() time.Duration {
return climb.points[len(climb.points)-1].Timestamp.Sub(climb.points[0].Timestamp)
}
+
+func (climb Climb) Speed() float64 {
+ return (climb.End().DistanceM - climb.Start().DistanceM) / climb.Duration().Seconds()
+}
+
func (climb Climb) Start() Point {
return climb.points[0]
}
diff --git a/ride/climb_test.go b/ride/climb_test.go
index 75aeb0e..ad00d65 100644
--- a/ride/climb_test.go
+++ b/ride/climb_test.go
@@ -9,7 +9,6 @@ import (
"github.com/martinlehoux/biking_home/ride"
"github.com/martinlehoux/kagamigo/kcore"
"github.com/stretchr/testify/assert"
- "github.com/tkrajina/gpxgo/gpx"
)
type RideBuilderSection struct {
@@ -60,6 +59,8 @@ func (b RideBuilder) Build() ride.Ride {
return ride.FromPoints(points)
}
+var parser = ride.GPXRideParser{}
+
func TestClimbThenFalseFlat(t *testing.T) {
r := RideBuilder{precision: 100}.WithSection("2km at 7%").WithSection("10km at 1%").Build()
climbs := r.AllClimbs()
@@ -77,9 +78,8 @@ func TestSmallClimbWithDescentInsideFalseFlat(t *testing.T) {
}
func TestPogacar20220721(t *testing.T) {
- gpxContent, err := gpx.ParseFile("../examples/2022-07-21.Pogacar.gpx")
+ r, err := ride.ParseFile(parser, "../examples/2022-07-21.Pogacar.gpx")
assert.NoError(t, err)
- r := ride.FromGPX(gpxContent)
climbs := r.AllClimbs()
assert.Len(t, climbs, 6)
@@ -92,9 +92,8 @@ func TestPogacar20220721(t *testing.T) {
}
func TestBouclesVerdon2024(t *testing.T) {
- gpxContent, err := gpx.ParseFile("../examples/2024-06-29.BouclesVerdon.gpx")
+ r, err := ride.ParseFile(parser, "../examples/2024-06-29.BouclesVerdon.gpx")
assert.NoError(t, err)
- r := ride.FromGPX(gpxContent)
climbs := r.AllClimbs()
assert.Len(t, climbs, 6)
@@ -107,9 +106,8 @@ func TestBouclesVerdon2024(t *testing.T) {
}
func TestMimetArbois20241229(t *testing.T) {
- gpxContent, err := gpx.ParseFile("../examples/2024-12-29.MimetArbois.gpx")
+ r, err := ride.ParseFile(parser, "../examples/2024-12-29.MimetArbois.gpx")
assert.NoError(t, err)
- r := ride.FromGPX(gpxContent)
climbs := r.AllClimbs()
assert.Len(t, climbs, 2)
@@ -118,9 +116,8 @@ func TestMimetArbois20241229(t *testing.T) {
}
func TestMimetArbois20250104(t *testing.T) {
- gpxContent, err := gpx.ParseFile("../examples/2025-01-04.MimetArbois.gpx")
+ r, err := ride.ParseFile(parser, "../examples/2025-01-04.MimetArbois.gpx")
assert.NoError(t, err)
- r := ride.FromGPX(gpxContent)
climbs := r.AllClimbs()
assert.Len(t, climbs, 2)
@@ -130,9 +127,8 @@ func TestMimetArbois20250104(t *testing.T) {
// https://www.strava.com/activities/9282265844
func TestAlpesVerdonTour20230617(t *testing.T) {
- gpxContent, err := gpx.ParseFile("../examples/2023-06-17.AlpesVerdonTour.gpx")
+ r, err := ride.ParseFile(parser, "../examples/2023-06-17.AlpesVerdonTour.gpx")
assert.NoError(t, err)
- r := ride.FromGPX(gpxContent)
climbs := r.AllClimbs()
assert.Len(t, climbs, 6)
diff --git a/ride/index_test.go b/ride/index_test.go
index e95f030..7b3dc34 100644
--- a/ride/index_test.go
+++ b/ride/index_test.go
@@ -7,28 +7,24 @@ import (
"github.com/bradleyjkemp/cupaloy"
"github.com/martinlehoux/biking_home/ride"
"github.com/stretchr/testify/assert"
- "github.com/tkrajina/gpxgo/gpx"
)
func TestRideClimbLaCride(t *testing.T) {
- gpxContent, err := gpx.ParseFile("../examples/activity_18043356988.gpx")
+ ride19Jan, err := ride.ParseFile(parser, "../examples/activity_18043356988.gpx")
assert.NoError(t, err)
- ride19Jan := ride.FromGPX(gpxContent)
- gpxContent, err = gpx.ParseFile("../examples/activity_18679866717.gpx")
+ ride30Mar, err := ride.ParseFile(parser, "../examples/activity_18679866717.gpx")
assert.NoError(t, err)
- ride30Mar := ride.FromGPX(gpxContent)
- gpxContent, err = gpx.ParseFile("../examples/activity_18880605641.gpx")
+ ride20Apr, err := ride.ParseFile(parser, "../examples/activity_18880605641.gpx")
assert.NoError(t, err)
- ride20Apr := ride.FromGPX(gpxContent)
index := ride.NewFlatRideClimbsIndex(30)
index.Insert(ride19Jan)
index.Insert(ride30Mar)
logs := []string{}
for _, climb := range ride20Apr.AllClimbs() {
+ logs = append(logs, fmt.Sprintf("Climb %s (%s, %.1fkm/h)", climb, climb.Duration(), climb.Speed()*3.6))
if len(index.Similar(ride20Apr, climb)) > 0 {
- logs = append(logs, fmt.Sprintf("Found similar climbs to %s", climb))
for _, similar := range index.Similar(ride20Apr, climb) {
- logs = append(logs, fmt.Sprintf(" %s", similar))
+ logs = append(logs, fmt.Sprintf(" %s (%s, %.1fkm/h)", similar, similar.Duration(), similar.Speed()*3.6))
}
}
}
diff --git a/ride/ride.go b/ride/ride.go
index 094a8f6..f5a92a5 100644
--- a/ride/ride.go
+++ b/ride/ride.go
@@ -1,6 +1,9 @@
package ride
import (
+ "errors"
+ "io"
+ "os"
"time"
"github.com/jftuga/geodist"
@@ -8,6 +11,7 @@ import (
"github.com/tkrajina/gpxgo/gpx"
"gonum.org/v1/plot"
"gonum.org/v1/plot/plotter"
+ "gonum.org/v1/plot/plotutil"
"gonum.org/v1/plot/vg"
)
@@ -26,7 +30,26 @@ func (r *Ride) check() {
kcore.Assert(len(r.points) > 0, "no points in ride")
}
-func FromGPX(content *gpx.GPX) Ride {
+type RideParser interface {
+ Parse(reader io.Reader) (Ride, error)
+}
+
+func ParseFile(parser RideParser, filename string) (Ride, error) {
+ f, err := os.Open(filename)
+ if err != nil {
+ return Ride{}, err
+ }
+ defer f.Close()
+ return parser.Parse(f)
+}
+
+type GPXRideParser struct{}
+
+func (p GPXRideParser) Parse(reader io.Reader) (Ride, error) {
+ content, err := gpx.Parse(reader)
+ if err != nil {
+ return Ride{}, err
+ }
segment := content.Tracks[0].Segments[0]
points := make([]Point, len(segment.Points))
distance := 0.0
@@ -34,13 +57,17 @@ func FromGPX(content *gpx.GPX) Ride {
if i != 0 {
distance += p.Distance2D(&segment.Points[i-1])
}
- kcore.Assert(i == 0 || distance > 0, "zero distance")
- kcore.Assert(p.Elevation.NotNull(), "points without elevation")
+ if i != 0 && distance == 0 {
+ return Ride{}, errors.New("zero distance")
+ }
+ if p.Elevation.Null() {
+ return Ride{}, errors.New("points without elevation")
+ }
points[i] = Point{DistanceM: distance, ElevationM: p.Elevation.Value(), Coord: geodist.Coord{Lat: p.Latitude, Lon: p.Longitude}, Timestamp: p.Timestamp}
}
ride := Ride{points}
ride.check()
- return ride
+ return ride, nil
}
func FromPoints(points []Point) Ride {
@@ -99,3 +126,66 @@ func Plot(r *Ride, outputFile string) {
err = p.Save(10*vg.Inch, 4*vg.Inch, outputFile)
kcore.Expect(err, "failed to save plot")
}
+
+func PlotScore(r *Ride, startKm, endKm float64, outputFile string) {
+ r.check()
+
+ startIndex := 0
+ for i, p := range r.points {
+ if p.DistanceM >= startKm*1000 {
+ startIndex = i
+ break
+ }
+ }
+
+ pts1 := make(plotter.XYs, 0)
+ endIndex := 0
+ for i := startIndex + 1; i < len(r.points); i++ {
+ if r.points[i].DistanceM > endKm*1000 {
+ endIndex = i
+ break
+ }
+ score1 := Score(r.points, startIndex, i)
+ pts1 = append(pts1, plotter.XY{
+ X: r.points[i].DistanceM / 1000, // Convert distance to kilometers
+ Y: score1,
+ })
+ }
+ pts2 := make(plotter.XYs, 0)
+ for i := startIndex; i < endIndex; i++ {
+ score2 := Score(r.points, i, endIndex)
+ pts2 = append(pts2, plotter.XY{
+ X: r.points[i].DistanceM / 1000, // Convert distance to kilometers
+ Y: score2,
+ })
+ }
+
+ p := plot.New()
+ p.Title.Text = "Ride Score Profile"
+ p.X.Label.Text = "Distance (km)"
+ p.Y.Label.Text = "Score"
+
+ line1, err := plotter.NewLine(pts1)
+ kcore.Expect(err, "failed to create line plot for score1")
+ line1.Color = plotutil.Color(0) // First line color
+ maxIndex := 0
+ maxValue := pts1[0].Y
+ for i, pt := range pts1 {
+ if pt.Y > maxValue {
+ maxValue = pt.Y
+ maxIndex = i
+ }
+ }
+ println("Max value of line 1 is at distance:", pts1[maxIndex].X, "km")
+
+ line2, err := plotter.NewLine(pts2)
+ kcore.Expect(err, "failed to create line plot for score2")
+ line2.Color = plotutil.Color(1) // Second line color
+
+ p.Add(line1, line2)
+ p.Legend.Add("Score (start to i)", line1)
+ p.Legend.Add("Score (i to end)", line2)
+
+ err = p.Save(10*vg.Inch, 4*vg.Inch, outputFile)
+ kcore.Expect(err, "failed to save plot")
+}