summaryrefslogtreecommitdiff
path: root/ride/climb.go
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-07 14:58:02 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-07 14:58:02 +0200
commit22a58e96dddd326f4c331c2a1bdea46f808ae3ad (patch)
tree08222960e60a9e6db843a0efe37e93b7f67913da /ride/climb.go
parent4b90740c41afb43d11e4dd2c0c9ed91654b7ce43 (diff)
refactor: Store rides in columns
Diffstat (limited to 'ride/climb.go')
-rw-r--r--ride/climb.go104
1 files changed, 59 insertions, 45 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
}