From d26aa2dca1b14b7d52070825bfc73c5e8aed7a16 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Fri, 21 Aug 2026 11:04:57 +0200 Subject: refactor(ride,rideanalysis): replace magic numbers with named constants --- ride/climb.go | 67 +++++++++++++++++++++++++++++++++++++++++++++-------------- ride/ride.go | 8 +++++-- 2 files changed, 57 insertions(+), 18 deletions(-) (limited to 'ride') 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++ } -- cgit v1.3