summaryrefslogtreecommitdiff
path: root/ride
diff options
context:
space:
mode:
Diffstat (limited to 'ride')
-rw-r--r--ride/climb.go104
-rw-r--r--ride/climb_test.go17
-rw-r--r--ride/difficulty_test.go2
-rw-r--r--ride/index.go4
-rw-r--r--ride/parser_test.go4
-rw-r--r--ride/ride.go131
6 files changed, 148 insertions, 114 deletions
diff --git a/ride/climb.go b/ride/climb.go
index d5a1718..9f38038 100644
--- a/ride/climb.go
+++ b/ride/climb.go
@@ -7,51 +7,67 @@ import (
"slices"
"time"
+ "github.com/jftuga/geodist"
"github.com/martinlehoux/kagamigo/kcore"
)
const ClimbDistanceMinimum = 500
type Climb struct {
+ ride Ride
rideStart int
rideEnd int
- points []Point
Name string
}
func (climb Climb) Duration() time.Duration {
- return climb.points[len(climb.points)-1].Timestamp.Sub(climb.points[0].Timestamp)
+ return climb.ride.Timestamp(climb.rideEnd).Sub(climb.ride.Timestamp(climb.rideStart))
}
func (climb Climb) Speed() float64 {
- return (climb.End().DistanceM - climb.Start().DistanceM) / climb.Duration().Seconds()
+ return (climb.EndDistanceM() - climb.StartDistanceM()) / climb.Duration().Seconds()
}
-func (climb Climb) Start() Point {
- return climb.points[0]
+func (climb Climb) StartIndex() int { return climb.rideStart }
+
+func (climb Climb) EndIndex() int { return climb.rideEnd }
+
+func (climb Climb) StartDistanceM() float64 {
+ return climb.ride.DistanceM(climb.rideStart)
}
-func (climb Climb) End() Point {
- return climb.points[len(climb.points)-1]
+func (climb Climb) EndDistanceM() float64 {
+ return climb.ride.DistanceM(climb.rideEnd)
}
-// Top returns the highest point inside the climb, used as the crest anchor
-// when naming the climb after a pass.
-func (climb Climb) Top() Point {
- top := climb.points[0]
- for _, point := range climb.points {
- if point.ElevationM > top.ElevationM {
- top = point
+// TopIndex returns the highest point inside the climb, used as the crest anchor.
+func (climb Climb) TopIndex() int {
+ top := climb.rideStart
+ for i := climb.rideStart; i <= climb.rideEnd; i++ {
+ if climb.ride.ElevationM(i) > climb.ride.ElevationM(top) {
+ top = i
}
}
return top
}
+func (climb Climb) TopDistanceM() float64 {
+ return climb.ride.DistanceM(climb.TopIndex())
+}
+
+func (climb Climb) TopElevationM() float64 {
+ return climb.ride.ElevationM(climb.TopIndex())
+}
+
+func (climb Climb) TopCoord() geodist.Coord {
+ return climb.ride.Coord(climb.TopIndex())
+}
+
func (climb Climb) String() string {
- start := climb.points[0]
- end := climb.points[len(climb.points)-1]
- score := Score(climb.points, 0, len(climb.points)-1)
- body := fmt.Sprintf("%.1fkm-%.1fkm: %.1fkm at %.1f%% (%d pts - %s)", start.DistanceM/1000, end.DistanceM/1000, (end.DistanceM-start.DistanceM)/1000, Slope(start, end)*100, int(score), Category(score))
+ startDistance := climb.StartDistanceM()
+ endDistance := climb.EndDistanceM()
+ score := climb.Score()
+ body := fmt.Sprintf("%.1fkm-%.1fkm: %.1fkm at %.1f%% (%d pts - %s)", startDistance/1000, endDistance/1000, (endDistance-startDistance)/1000, Slope(climb.ride, climb.rideStart, climb.rideEnd)*100, int(score), Category(score))
if climb.Name == "" {
return body
}
@@ -59,24 +75,24 @@ func (climb Climb) String() string {
}
func (climb Climb) Score() float64 {
- return Score(climb.points, 0, len(climb.points)-1)
+ return Score(climb.ride, climb.rideStart, climb.rideEnd)
}
func (climb Climb) DifficultyScore() float64 {
- return difficultyScore(climb.points)
+ return difficultyScore(climb.ride, climb.rideStart, climb.rideEnd)
}
-func Slope(start, end Point) float64 {
- return (end.ElevationM - start.ElevationM) / (end.DistanceM - start.DistanceM)
+func Slope(r Ride, start, end int) float64 {
+ return (r.ElevationM(end) - r.ElevationM(start)) / (r.DistanceM(end) - r.DistanceM(start))
}
-func Score(points []Point, start int, end int) float64 {
+func Score(r Ride, start, end int) float64 {
kcore.Assert(end > start, "no points for score")
- distance := points[end].DistanceM - points[start].DistanceM
+ distance := r.DistanceM(end) - r.DistanceM(start)
if distance == 0 {
return 0
}
- dElevation := points[end].ElevationM - points[start].ElevationM
+ dElevation := r.ElevationM(end) - r.ElevationM(start)
return math.Abs(dElevation) * dElevation / distance * 100.0 * 100.0 / 1000.0
}
@@ -98,13 +114,13 @@ func Category(score float64) string {
}
}
-func bestClimbBetween(points []Point, start int, end int) Climb {
+func bestClimbBetween(r Ride, start, end int) Climb {
kcore.Assert(end > start, "empty points")
- bestScore := Score(points, start, end)
+ bestScore := Score(r, start, end)
bestStart := start
for i := start; i < end; i++ {
- score := Score(points, i, end)
+ score := Score(r, i, end)
if score > bestScore {
bestStart = i
bestScore = score
@@ -112,54 +128,52 @@ func bestClimbBetween(points []Point, start int, end int) Climb {
}
bestEnd := end
for i := end; i > bestStart; i-- {
- score := Score(points, bestStart, i)
+ score := Score(r, bestStart, i)
if score > bestScore {
bestEnd = i
bestScore = score
}
}
for i := bestStart; i < bestEnd; i++ {
- score := Score(points, i, bestEnd)
+ score := Score(r, i, bestEnd)
if score > bestScore {
bestStart = i
bestScore = score
}
}
kcore.Assert(bestStart < bestEnd, "empty climb")
- climb := Climb{rideStart: bestStart, rideEnd: bestEnd, points: points[bestStart : bestEnd+1]}
-
- return climb
+ return Climb{ride: r, rideStart: bestStart, rideEnd: bestEnd}
}
-func climbsBetween(points []Point, start int, end int) []Climb {
+func climbsBetween(r Ride, start, end int) []Climb {
climbs := []Climb{}
- if points[end].DistanceM-points[start].DistanceM < ClimbDistanceMinimum {
+ if r.DistanceM(end)-r.DistanceM(start) < ClimbDistanceMinimum {
return climbs
}
- slog.Debug("Searching climbs between", slog.Int("start", int(points[start].DistanceM)), slog.Int("end", int(points[end].DistanceM)))
+ slog.Debug("Searching climbs between", slog.Int("start", int(r.DistanceM(start))), slog.Int("end", int(r.DistanceM(end))))
highest := start
for i := start; i <= end; i++ {
- if points[i].ElevationM > points[highest].ElevationM {
+ if r.ElevationM(i) > r.ElevationM(highest) {
highest = i
}
}
// TODO: Use descent to reduce recursion
- if points[highest].DistanceM-points[start].DistanceM < ClimbDistanceMinimum {
- return climbsBetween(points, start+1, end)
+ if r.DistanceM(highest)-r.DistanceM(start) < ClimbDistanceMinimum {
+ return climbsBetween(r, start+1, end)
}
- climb := bestClimbBetween(points, start, highest)
- if climb.Score() >= 35 && climb.End().DistanceM-climb.Start().DistanceM >= ClimbDistanceMinimum {
- slog.Debug("Found climb between", slog.Int("start", int(climb.Start().DistanceM)), slog.Int("end", int(climb.End().DistanceM)))
+ climb := bestClimbBetween(r, start, highest)
+ if climb.Score() >= 35 && climb.EndDistanceM()-climb.StartDistanceM() >= ClimbDistanceMinimum {
+ slog.Debug("Found climb between", slog.Int("start", int(climb.StartDistanceM())), slog.Int("end", int(climb.EndDistanceM())))
climbs = append(climbs, climb)
}
- climbs = append(climbs, climbsBetween(points, start, climb.rideStart)...)
- climbs = append(climbs, climbsBetween(points, climb.rideEnd, end)...)
+ climbs = append(climbs, climbsBetween(r, start, climb.rideStart)...)
+ climbs = append(climbs, climbsBetween(r, climb.rideEnd, end)...)
return climbs
}
func (ride *Ride) AllClimbs() []Climb {
- climbs := climbsBetween(ride.points, 0, len(ride.points)-1)
+ climbs := climbsBetween(*ride, 0, ride.Len()-1)
slices.SortFunc(climbs, climbCmpStart)
return climbs
}
diff --git a/ride/climb_test.go b/ride/climb_test.go
index ad00d65..3dd6e53 100644
--- a/ride/climb_test.go
+++ b/ride/climb_test.go
@@ -6,6 +6,7 @@ import (
"testing"
"time"
+ "github.com/jftuga/geodist"
"github.com/martinlehoux/biking_home/ride"
"github.com/martinlehoux/kagamigo/kcore"
"github.com/stretchr/testify/assert"
@@ -40,7 +41,10 @@ func (b RideBuilder) WithSection(input string) RideBuilder {
}
func (b RideBuilder) Build() ride.Ride {
- points := []ride.Point{{}}
+ distances := []float64{0}
+ elevations := []float64{0}
+ coords := []geodist.Coord{{}}
+ timestamps := []time.Time{{}}
distance := 0.0
elevation := 0.0
for _, section := range b.sections {
@@ -48,15 +52,14 @@ func (b RideBuilder) Build() ride.Ride {
for curSecDist < section.distance {
curSecDist += b.precision
elevation += b.precision * section.slope
- points = append(points, ride.Point{
- DistanceM: distance + curSecDist,
- ElevationM: elevation,
- Timestamp: points[len(points)-1].Timestamp.Add(time.Second),
- })
+ distances = append(distances, distance+curSecDist)
+ elevations = append(elevations, elevation)
+ coords = append(coords, geodist.Coord{})
+ timestamps = append(timestamps, timestamps[len(timestamps)-1].Add(time.Second))
}
distance += curSecDist
}
- return ride.FromPoints(points)
+ return ride.FromColumns(distances, elevations, coords, timestamps)
}
var parser = ride.GPXRideParser{}
diff --git a/ride/difficulty_test.go b/ride/difficulty_test.go
index 5ba14c7..4b72037 100644
--- a/ride/difficulty_test.go
+++ b/ride/difficulty_test.go
@@ -54,6 +54,6 @@ func TestDifficultyScoreHautacamClimbfinderReference(t *testing.T) {
climbs := r.AllClimbs()
assert.Len(t, climbs, 6)
hautacam := climbs[5]
- assert.InDelta(t, 128.6, hautacam.Start().DistanceM/1000, 0.1)
+ assert.InDelta(t, 128.6, hautacam.StartDistanceM()/1000, 0.1)
assert.InDelta(t, 930.0, hautacam.DifficultyScore(), 10)
}
diff --git a/ride/index.go b/ride/index.go
index 79f9212..17564ca 100644
--- a/ride/index.go
+++ b/ride/index.go
@@ -26,8 +26,8 @@ func (index *FlatRideClimbsIndex) Insert(ride Ride) {
func (index *FlatRideClimbsIndex) Similar(ride Ride, climb Climb) []Climb {
results := make([]Climb, 0)
for _, c := range index.climbs {
- _, startDistance := geodist.HaversineDistance(c.Start().Coord, climb.Start().Coord)
- _, endDistance := geodist.HaversineDistance(c.End().Coord, climb.End().Coord)
+ _, startDistance := geodist.HaversineDistance(c.ride.Coord(c.StartIndex()), climb.ride.Coord(climb.StartIndex()))
+ _, endDistance := geodist.HaversineDistance(c.ride.Coord(c.EndIndex()), climb.ride.Coord(climb.EndIndex()))
if startDistance <= index.sensitivity/1000 && endDistance <= index.sensitivity/1000 {
results = append(results, c)
}
diff --git a/ride/parser_test.go b/ride/parser_test.go
index db4907d..96c837e 100644
--- a/ride/parser_test.go
+++ b/ride/parser_test.go
@@ -22,8 +22,8 @@ func TestGPXRideParserSkipsStationaryPoints(t *testing.T) {
parsed, err := (ride.GPXRideParser{}).Parse(strings.NewReader(data))
require.NoError(t, err)
- assert.Len(t, parsed.Points(), 2)
- assert.Greater(t, parsed.Points()[1].DistanceM, 0.0)
+ assert.Equal(t, 2, parsed.Len())
+ assert.Greater(t, parsed.DistanceM(1), 0.0)
}
func TestGPXRideParserRejectsStationaryRide(t *testing.T) {
diff --git a/ride/ride.go b/ride/ride.go
index 9ba644e..311e933 100644
--- a/ride/ride.go
+++ b/ride/ride.go
@@ -16,25 +16,32 @@ import (
"gonum.org/v1/plot/vg"
)
-type Point struct {
- DistanceM float64
- ElevationM float64
- Coord geodist.Coord
- Timestamp time.Time
-}
-
type Ride struct {
- points []Point
+ distances []float64
+ elevations []float64
+ coords []geodist.Coord
+ timestamps []time.Time
}
func (r *Ride) check() {
- kcore.Assert(len(r.points) > 0, "no points in ride")
+ kcore.Assert(r.Len() > 0, "no points in ride")
+ kcore.Assert(len(r.distances) == len(r.elevations), "ride columns have different lengths")
+ kcore.Assert(len(r.distances) == len(r.coords), "ride columns have different lengths")
+ kcore.Assert(len(r.distances) == len(r.timestamps), "ride columns have different lengths")
}
-func (r *Ride) Points() []Point {
- return r.points
+func (r Ride) Len() int {
+ return len(r.distances)
}
+func (r Ride) DistanceM(i int) float64 { return r.distances[i] }
+
+func (r Ride) ElevationM(i int) float64 { return r.elevations[i] }
+
+func (r Ride) Coord(i int) geodist.Coord { return r.coords[i] }
+
+func (r Ride) Timestamp(i int) time.Time { return r.timestamps[i] }
+
type RideParser interface {
Parse(reader io.Reader) (Ride, error)
}
@@ -62,13 +69,19 @@ func (p GPXRideParser) Parse(reader io.Reader) (Ride, error) {
if len(segment.Points) == 0 {
return Ride{}, errors.New("ride has no track points")
}
- points := make([]Point, 0, len(segment.Points))
+ distances := make([]float64, 0, len(segment.Points))
+ elevations := make([]float64, 0, len(segment.Points))
+ coords := make([]geodist.Coord, 0, len(segment.Points))
+ timestamps := make([]time.Time, 0, len(segment.Points))
distance := 0.0
previous := segment.Points[0]
if previous.Elevation.Null() {
return Ride{}, errors.New("points without elevation")
}
- points = append(points, Point{DistanceM: 0, ElevationM: previous.Elevation.Value(), Coord: geodist.Coord{Lat: previous.Latitude, Lon: previous.Longitude}, Timestamp: previous.Timestamp})
+ distances = append(distances, 0)
+ elevations = append(elevations, previous.Elevation.Value())
+ coords = append(coords, geodist.Coord{Lat: previous.Latitude, Lon: previous.Longitude})
+ timestamps = append(timestamps, previous.Timestamp)
for i := 1; i < len(segment.Points); i++ {
p := segment.Points[i]
distance += p.Distance2D(&previous)
@@ -79,18 +92,21 @@ func (p GPXRideParser) Parse(reader io.Reader) (Ride, error) {
if p.Elevation.Null() {
return Ride{}, errors.New("points without elevation")
}
- points = append(points, Point{DistanceM: distance, ElevationM: p.Elevation.Value(), Coord: geodist.Coord{Lat: p.Latitude, Lon: p.Longitude}, Timestamp: p.Timestamp})
+ distances = append(distances, distance)
+ elevations = append(elevations, p.Elevation.Value())
+ coords = append(coords, geodist.Coord{Lat: p.Latitude, Lon: p.Longitude})
+ timestamps = append(timestamps, p.Timestamp)
}
- if len(points) < 2 {
+ if len(distances) < 2 {
return Ride{}, errors.New("zero distance")
}
- ride := Ride{points}
+ ride := Ride{distances: distances, elevations: elevations, coords: coords, timestamps: timestamps}
ride.check()
return ride, nil
}
-func FromPoints(points []Point) Ride {
- ride := Ride{points}
+func FromColumns(distances []float64, elevations []float64, coords []geodist.Coord, timestamps []time.Time) Ride {
+ ride := Ride{distances: distances, elevations: elevations, coords: coords, timestamps: timestamps}
ride.check()
return ride
}
@@ -98,42 +114,43 @@ func FromPoints(points []Point) Ride {
func (r *Ride) ScoreFromKm(start, end float64) float64 {
i := 0
j := 0
- for k, p := range r.points {
- if i == 0 && p.DistanceM >= start*1000 {
+ for k, distance := range r.distances {
+ if i == 0 && distance >= start*1000 {
i = k
}
- if j == 0 && p.DistanceM >= end*1000 {
+ if j == 0 && distance >= end*1000 {
j = k
break
}
}
- return Score(r.points, i, j)
+ return Score(*r, i, j)
}
func (r *Ride) DifficultyScore() float64 {
- return difficultyScore(r.points)
+ return difficultyScore(*r, 0, r.Len()-1)
}
-func difficultyScore(points []Point) float64 {
- if len(points) < 2 {
+func difficultyScore(r Ride, startIndex, endIndex int) float64 {
+ if endIndex-startIndex < 1 {
return 0
}
- last := points[len(points)-1]
- if last.DistanceM <= 0 {
+ startDistance := r.DistanceM(startIndex)
+ lastDistance := r.DistanceM(endIndex)
+ if lastDistance <= startDistance {
return 0
}
score := 0.0
- i := 0
- for start := points[0].DistanceM; start < last.DistanceM; start += 100 {
- end := math.Min(start+100, last.DistanceM)
- for i < len(points)-1 && points[i+1].DistanceM <= start {
+ i := startIndex
+ for start := startDistance; start < lastDistance; start += 100 {
+ end := math.Min(start+100, lastDistance)
+ for i < endIndex && r.DistanceM(i+1) <= start {
i++
}
- startElevation := interpolateElevation(points, i, start)
- for i < len(points)-1 && points[i+1].DistanceM < end {
+ startElevation := interpolateElevation(r, i, start)
+ for i < endIndex && r.DistanceM(i+1) < end {
i++
}
- endElevation := interpolateElevation(points, i, end)
+ endElevation := interpolateElevation(r, i, end)
slope := (endElevation - startElevation) / (end - start)
if slope > 0 {
score += (end - start) / 1000 * (slope * 100) * (slope * 100)
@@ -142,37 +159,37 @@ func difficultyScore(points []Point) float64 {
return score
}
-func interpolateElevation(points []Point, i int, distance float64) float64 {
- if i+1 >= len(points) {
- return points[i].ElevationM
+func interpolateElevation(r Ride, i int, distance float64) float64 {
+ if i+1 >= r.Len() {
+ return r.ElevationM(i)
}
- start := points[i]
- end := points[i+1]
- t := (distance - start.DistanceM) / (end.DistanceM - start.DistanceM)
- return start.ElevationM + t*(end.ElevationM-start.ElevationM)
+ startDistance := r.DistanceM(i)
+ endDistance := r.DistanceM(i + 1)
+ t := (distance - startDistance) / (endDistance - startDistance)
+ return r.ElevationM(i) + t*(r.ElevationM(i+1)-r.ElevationM(i))
}
func (r *Ride) ClimbFromDist(startDist, endDist float64) Climb {
start, end := 0, 0
- for i, p := range r.points {
- if start == 0 && p.DistanceM >= startDist {
+ for i, distance := range r.distances {
+ if start == 0 && distance >= startDist {
start = i
}
- if end == 0 && p.DistanceM >= endDist {
+ if end == 0 && distance >= endDist {
end = i
break
}
}
- return Climb{rideStart: start, rideEnd: end, points: r.points[start : end+1]}
+ return Climb{ride: *r, rideStart: start, rideEnd: end}
}
func Plot(r *Ride, outputFile string) {
r.check()
- pts := make(plotter.XYs, len(r.points))
- for i, p := range r.points {
- pts[i].X = p.DistanceM / 1000 // Convert distance to kilometers
- pts[i].Y = p.ElevationM
+ pts := make(plotter.XYs, r.Len())
+ for i := 0; i < r.Len(); i++ {
+ pts[i].X = r.DistanceM(i) / 1000 // Convert distance to kilometers
+ pts[i].Y = r.ElevationM(i)
}
p := plot.New()
@@ -192,8 +209,8 @@ func PlotScore(r *Ride, startKm, endKm float64, outputFile string) {
r.check()
startIndex := 0
- for i, p := range r.points {
- if p.DistanceM >= startKm*1000 {
+ for i := 0; i < r.Len(); i++ {
+ if r.DistanceM(i) >= startKm*1000 {
startIndex = i
break
}
@@ -201,22 +218,22 @@ func PlotScore(r *Ride, startKm, endKm float64, outputFile string) {
pts1 := make(plotter.XYs, 0)
endIndex := 0
- for i := startIndex + 1; i < len(r.points); i++ {
- if r.points[i].DistanceM > endKm*1000 {
+ for i := startIndex + 1; i < r.Len(); i++ {
+ if r.DistanceM(i) > endKm*1000 {
endIndex = i
break
}
- score1 := Score(r.points, startIndex, i)
+ score1 := Score(*r, startIndex, i)
pts1 = append(pts1, plotter.XY{
- X: r.points[i].DistanceM / 1000, // Convert distance to kilometers
+ X: r.DistanceM(i) / 1000, // Convert distance to kilometers
Y: score1,
})
}
pts2 := make(plotter.XYs, 0)
for i := startIndex; i < endIndex; i++ {
- score2 := Score(r.points, i, endIndex)
+ score2 := Score(*r, i, endIndex)
pts2 = append(pts2, plotter.XY{
- X: r.points[i].DistanceM / 1000, // Convert distance to kilometers
+ X: r.DistanceM(i) / 1000, // Convert distance to kilometers
Y: score2,
})
}