aboutsummaryrefslogtreecommitdiff
path: root/ride
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-21 11:04:57 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-21 11:04:57 +0200
commitd26aa2dca1b14b7d52070825bfc73c5e8aed7a16 (patch)
tree5d2ad13f342316dcdb1258ecd8c6e739d9adf08d /ride
parent44f7333d9deef81d3468e20b378c63692a9406ec (diff)
refactor(ride,rideanalysis): replace magic numbers with named constants
Diffstat (limited to 'ride')
-rw-r--r--ride/climb.go67
-rw-r--r--ride/ride.go8
2 files changed, 57 insertions, 18 deletions
diff --git a/ride/climb.go b/ride/climb.go
index 56e47c6..8852cbf 100644
--- a/ride/climb.go
+++ b/ride/climb.go
@@ -13,6 +13,41 @@ import (
const ClimbDistanceMinimum = 500
+// Category is the difficulty class of a climb, ordered from easiest to
+// hardest.
+type Category int
+
+const (
+ CategoryNo Category = iota
+ CategoryFour
+ CategoryThree
+ CategoryTwo
+ CategoryOne
+ CategoryHC
+)
+
+type categoryInfo struct {
+ label string
+ maxScore float64
+}
+
+// categories describes each Category: its display label and the exclusive
+// Cotacol score ceiling it applies to. A score at or above CategoryOne's
+// ceiling is Hors Catégorie.
+var categories = map[Category]categoryInfo{
+ CategoryNo: {label: "NO", maxScore: 35},
+ CategoryFour: {label: "Cat 4", maxScore: 80},
+ CategoryThree: {label: "Cat 3", maxScore: 180},
+ CategoryTwo: {label: "Cat 2", maxScore: 250},
+ CategoryOne: {label: "Cat 1", maxScore: 600},
+ // Hors Catégorie has no ceiling.
+ CategoryHC: {label: "HC", maxScore: math.Inf(+1)},
+}
+
+// climbDetectionMinimumScore is the climb-detection score below which a
+// candidate segment is not reported as a climb.
+const climbDetectionMinimumScore = 35
+
type Climb struct {
ride Ride
rideStart int
@@ -80,7 +115,7 @@ func (climb Climb) String() string {
startDistance := climb.StartDistanceM()
endDistance := climb.EndDistanceM()
cotacol := climb.DifficultyScore()
- 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(cotacol), Category(cotacol))
+ 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(cotacol), CategoryOf(cotacol))
if climb.Name == "" {
return body
}
@@ -105,21 +140,21 @@ func climbDetectionScore(r Ride, start, end int) float64 {
return math.Abs(dElevation) * dElevation / distance * 100.0 * 100.0 / 1000.0
}
-func Category(cotacol float64) string {
- switch {
- case cotacol < 35:
- return "NO"
- case cotacol < 80:
- return "Cat 4"
- case cotacol < 180:
- return "Cat 3"
- case cotacol < 250:
- return "Cat 2"
- case cotacol < 600:
- return "Cat 1"
- default:
- return "HC"
+func (c Category) String() string {
+ return categories[c].label
+}
+
+// CategoryOf returns the difficulty class of a Cotacol score.
+func CategoryOf(cotacol float64) Category {
+ best := CategoryHC
+ bestMax := math.Inf(+1)
+ for category, info := range categories {
+ if cotacol < info.maxScore && info.maxScore < bestMax {
+ best = category
+ bestMax = info.maxScore
+ }
}
+ return best
}
func bestClimbBetween(r Ride, start, end int) Climb {
@@ -170,7 +205,7 @@ func climbsBetween(r Ride, start, end int) []Climb {
return climbsBetween(r, start+1, end)
}
climb := bestClimbBetween(r, start, highest)
- if climbDetectionScore(r, climb.rideStart, climb.rideEnd) >= 35 && climb.EndDistanceM()-climb.StartDistanceM() >= ClimbDistanceMinimum {
+ if climbDetectionScore(r, climb.rideStart, climb.rideEnd) >= climbDetectionMinimumScore && 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)
}
diff --git a/ride/ride.go b/ride/ride.go
index 278880f..a10d8a2 100644
--- a/ride/ride.go
+++ b/ride/ride.go
@@ -230,6 +230,10 @@ func metricValue(point gpx.GPXPoint, name string) (*float64, error) {
const CotacolAlgorithmVersion = "v1"
+// cotacolSegmentLengthM is the fixed length of the elevation segments the
+// Cotacol score is computed over.
+const cotacolSegmentLengthM = 100
+
func Cotacol(ride Ride) float64 {
return difficultyScore(ride, 0, ride.Len()-1)
}
@@ -245,8 +249,8 @@ func difficultyScore(r Ride, startIndex, endIndex int) float64 {
}
cotacol := 0.0
i := startIndex
- for start := startDistance; start < lastDistance; start += 100 {
- end := math.Min(start+100, lastDistance)
+ for start := startDistance; start < lastDistance; start += cotacolSegmentLengthM {
+ end := math.Min(start+cotacolSegmentLengthM, lastDistance)
for i < endIndex && r.DistanceM(i+1) <= start {
i++
}