diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-16 14:55:26 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-16 14:55:26 +0200 |
| commit | df7c9b5130ba193fc918a947a461f2a70dc2dd3c (patch) | |
| tree | 0366e0b60338e268d758511621b9adf721be8322 /ride | |
| parent | 53ab332e6d2afe9818914e0026310e4cadcc4202 (diff) | |
fix(web): use Cotacol for climb difficulty
Diffstat (limited to 'ride')
| -rw-r--r-- | ride/.snapshots/TestRideClimbLaCride | 6 | ||||
| -rw-r--r-- | ride/climb.go | 47 | ||||
| -rw-r--r-- | ride/climb_test.go | 42 | ||||
| -rw-r--r-- | ride/ride.go | 21 |
4 files changed, 48 insertions, 68 deletions
diff --git a/ride/.snapshots/TestRideClimbLaCride b/ride/.snapshots/TestRideClimbLaCride index 456818e..d81ca17 100644 --- a/ride/.snapshots/TestRideClimbLaCride +++ b/ride/.snapshots/TestRideClimbLaCride @@ -1,5 +1,5 @@ ([]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)" + (string) (len=69) "Climb 17.1km-18.8km: 1.7km at 4.8% (45 pts - Cat 4) (5m50s, 17.0km/h)", + (string) (len=65) " 17.2km-18.8km: 1.6km at 4.9% (45 pts - Cat 4) (7m42s, 12.8km/h)", + (string) (len=71) "Climb 35.1km-40.1km: 5.1km at 4.3% (126 pts - Cat 3) (18m48s, 16.2km/h)" } diff --git a/ride/climb.go b/ride/climb.go index 788cd61..56e47c6 100644 --- a/ride/climb.go +++ b/ride/climb.go @@ -79,18 +79,14 @@ func (climb Climb) TopCoord() geodist.Coord { func (climb Climb) String() string { 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)) + 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)) if climb.Name == "" { return body } return climb.Name + ": " + body } -func (climb Climb) Score() float64 { - return Score(climb.ride, climb.rideStart, climb.rideEnd) -} - func (climb Climb) DifficultyScore() float64 { return difficultyScore(climb.ride, climb.rideStart, climb.rideEnd) } @@ -99,28 +95,27 @@ func Slope(r Ride, start, end int) float64 { return (r.ElevationM(end) - r.ElevationM(start)) / (r.DistanceM(end) - r.DistanceM(start)) } -func Score(r Ride, start, end int) float64 { - kcore.Assert(end > start, "no points for score") +func climbDetectionScore(r Ride, start, end int) float64 { + kcore.Assert(end > start, "no points for climb detection") distance := r.DistanceM(end) - r.DistanceM(start) if distance == 0 { return 0 } dElevation := r.ElevationM(end) - r.ElevationM(start) - return math.Abs(dElevation) * dElevation / distance * 100.0 * 100.0 / 1000.0 } -func Category(score float64) string { +func Category(cotacol float64) string { switch { - case score < 35: + case cotacol < 35: return "NO" - case score < 80: + case cotacol < 80: return "Cat 4" - case score < 180: + case cotacol < 180: return "Cat 3" - case score < 250: + case cotacol < 250: return "Cat 2" - case score < 600: + case cotacol < 600: return "Cat 1" default: return "HC" @@ -130,28 +125,28 @@ func Category(score float64) string { func bestClimbBetween(r Ride, start, end int) Climb { kcore.Assert(end > start, "empty points") - bestScore := Score(r, start, end) + bestDetectionScore := climbDetectionScore(r, start, end) bestStart := start for i := start; i < end; i++ { - score := Score(r, i, end) - if score > bestScore { + detectionScore := climbDetectionScore(r, i, end) + if detectionScore > bestDetectionScore { bestStart = i - bestScore = score + bestDetectionScore = detectionScore } } bestEnd := end for i := end; i > bestStart; i-- { - score := Score(r, bestStart, i) - if score > bestScore { + detectionScore := climbDetectionScore(r, bestStart, i) + if detectionScore > bestDetectionScore { bestEnd = i - bestScore = score + bestDetectionScore = detectionScore } } for i := bestStart; i < bestEnd; i++ { - score := Score(r, i, bestEnd) - if score > bestScore { + detectionScore := climbDetectionScore(r, i, bestEnd) + if detectionScore > bestDetectionScore { bestStart = i - bestScore = score + bestDetectionScore = detectionScore } } kcore.Assert(bestStart < bestEnd, "empty climb") @@ -175,7 +170,7 @@ func climbsBetween(r Ride, start, end int) []Climb { return climbsBetween(r, start+1, end) } climb := bestClimbBetween(r, start, highest) - if climb.Score() >= 35 && climb.EndDistanceM()-climb.StartDistanceM() >= ClimbDistanceMinimum { + if climbDetectionScore(r, climb.rideStart, climb.rideEnd) >= 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) } diff --git a/ride/climb_test.go b/ride/climb_test.go index 3dd6e53..feca001 100644 --- a/ride/climb_test.go +++ b/ride/climb_test.go @@ -86,12 +86,12 @@ func TestPogacar20220721(t *testing.T) { climbs := r.AllClimbs() assert.Len(t, climbs, 6) - assert.Equal(t, "0.6km-5.6km: 5.0km at 3.5% (62 pts - Cat 4)", climbs[0].String()) - assert.Equal(t, "42.9km-45.2km: 2.3km at 5.0% (59 pts - Cat 4)", climbs[1].String()) - assert.Equal(t, "60.0km-76.4km: 16.4km at 7.2% (854 pts - HC)", climbs[2].String()) - assert.Equal(t, "83.8km-85.9km: 2.0km at 5.4% (59 pts - Cat 4)", climbs[3].String()) - assert.Equal(t, "99.2km-109.3km: 10.2km at 8.4% (710 pts - HC)", climbs[4].String()) - assert.Equal(t, "128.6km-142.2km: 13.6km at 7.8% (832 pts - HC)", climbs[5].String()) + assert.Equal(t, "0.6km-5.6km: 5.0km at 3.5% (92 pts - Cat 3)", climbs[0].String()) + assert.Equal(t, "42.9km-45.2km: 2.3km at 5.0% (74 pts - Cat 4)", climbs[1].String()) + assert.Equal(t, "60.0km-76.4km: 16.4km at 7.2% (977 pts - HC)", climbs[2].String()) + assert.Equal(t, "83.8km-85.9km: 2.0km at 5.4% (79 pts - Cat 4)", climbs[3].String()) + assert.Equal(t, "99.2km-109.3km: 10.2km at 8.4% (751 pts - HC)", climbs[4].String()) + assert.Equal(t, "128.6km-142.2km: 13.6km at 7.8% (930 pts - HC)", climbs[5].String()) } func TestBouclesVerdon2024(t *testing.T) { @@ -100,12 +100,12 @@ func TestBouclesVerdon2024(t *testing.T) { climbs := r.AllClimbs() assert.Len(t, climbs, 6) - assert.Equal(t, "3.6km-13.8km: 10.3km at 2.1% (44 pts - Cat 4)", climbs[0].String()) // TODO: Not steep enough - assert.Equal(t, "34.7km-37.3km: 2.5km at 5.0% (62 pts - Cat 4)", climbs[1].String()) - assert.Equal(t, "41.9km-51.0km: 9.0km at 2.5% (55 pts - Cat 4)", climbs[2].String()) // TODO: Not steep enough - assert.Equal(t, "57.6km-60.8km: 3.1km at 6.3% (124 pts - Cat 3)", climbs[3].String()) - assert.Equal(t, "71.7km-74.7km: 3.0km at 5.3% (84 pts - Cat 3)", climbs[4].String()) - assert.Equal(t, "77.4km-78.7km: 1.3km at 5.3% (37 pts - Cat 4)", climbs[5].String()) + assert.Equal(t, "3.6km-13.8km: 10.3km at 2.1% (98 pts - Cat 3)", climbs[0].String()) // TODO: Not steep enough + assert.Equal(t, "34.7km-37.3km: 2.5km at 5.0% (67 pts - Cat 4)", climbs[1].String()) + assert.Equal(t, "41.9km-51.0km: 9.0km at 2.5% (113 pts - Cat 3)", climbs[2].String()) // TODO: Not steep enough + assert.Equal(t, "57.6km-60.8km: 3.1km at 6.3% (139 pts - Cat 3)", climbs[3].String()) + assert.Equal(t, "71.7km-74.7km: 3.0km at 5.3% (89 pts - Cat 3)", climbs[4].String()) + assert.Equal(t, "77.4km-78.7km: 1.3km at 5.3% (39 pts - Cat 4)", climbs[5].String()) } func TestMimetArbois20241229(t *testing.T) { @@ -114,8 +114,8 @@ func TestMimetArbois20241229(t *testing.T) { climbs := r.AllClimbs() assert.Len(t, climbs, 2) - assert.Equal(t, "3.9km-5.3km: 1.4km at 10.3% (153 pts - Cat 3)", climbs[0].String()) - assert.Equal(t, "14.4km-19.8km: 5.4km at 4.3% (98 pts - Cat 3)", climbs[1].String()) + assert.Equal(t, "3.9km-5.3km: 1.4km at 10.3% (165 pts - Cat 3)", climbs[0].String()) + assert.Equal(t, "14.4km-19.8km: 5.4km at 4.3% (113 pts - Cat 3)", climbs[1].String()) } func TestMimetArbois20250104(t *testing.T) { @@ -124,8 +124,8 @@ func TestMimetArbois20250104(t *testing.T) { climbs := r.AllClimbs() assert.Len(t, climbs, 2) - assert.Equal(t, "4.5km-5.9km: 1.4km at 10.1% (148 pts - Cat 3)", climbs[0].String()) - assert.Equal(t, "12.0km-20.2km: 8.2km at 2.9% (67 pts - Cat 4)", climbs[1].String()) // TODO: Not steep enough + assert.Equal(t, "4.5km-5.9km: 1.4km at 10.1% (159 pts - Cat 3)", climbs[0].String()) + assert.Equal(t, "12.0km-20.2km: 8.2km at 2.9% (148 pts - Cat 3)", climbs[1].String()) // TODO: Not steep enough } // https://www.strava.com/activities/9282265844 @@ -135,10 +135,10 @@ func TestAlpesVerdonTour20230617(t *testing.T) { climbs := r.AllClimbs() assert.Len(t, climbs, 6) - assert.Equal(t, "0.7km-18.6km: 17.9km at 3.5% (217 pts - Cat 2)", climbs[0].String()) // TODO: Should split in 2 (10km at 4.5% is Cat 2) - assert.Equal(t, "36.1km-41.6km: 5.4km at 5.8% (182 pts - Cat 2)", climbs[1].String()) + assert.Equal(t, "0.7km-18.6km: 17.9km at 3.5% (364 pts - Cat 1)", climbs[0].String()) // TODO: Should split in 2 (10km at 4.5% is Cat 2) + assert.Equal(t, "36.1km-41.6km: 5.4km at 5.8% (193 pts - Cat 2)", climbs[1].String()) assert.Equal(t, "47.0km-48.2km: 1.2km at 6.1% (45 pts - Cat 4)", climbs[2].String()) - assert.Equal(t, "51.9km-54.7km: 2.8km at 4.0% (45 pts - Cat 4)", climbs[3].String()) - assert.Equal(t, "86.7km-92.8km: 6.1km at 2.7% (44 pts - Cat 4)", climbs[4].String()) - assert.Equal(t, "114.1km-114.7km: 0.6km at 14.3% (118 pts - Cat 3)", climbs[5].String()) + assert.Equal(t, "51.9km-54.7km: 2.8km at 4.0% (48 pts - Cat 4)", climbs[3].String()) + assert.Equal(t, "86.7km-92.8km: 6.1km at 2.7% (60 pts - Cat 4)", climbs[4].String()) + assert.Equal(t, "114.1km-114.7km: 0.6km at 14.3% (119 pts - Cat 3)", climbs[5].String()) } diff --git a/ride/ride.go b/ride/ride.go index 3458483..152bca4 100644 --- a/ride/ride.go +++ b/ride/ride.go @@ -226,21 +226,6 @@ func metricValue(point gpx.GPXPoint, name string) (*float64, error) { return &value, nil } -func (r *Ride) ScoreFromKm(start, end float64) float64 { - i := 0 - j := 0 - for k, distance := range r.distances { - if i == 0 && distance >= start*1000 { - i = k - } - if j == 0 && distance >= end*1000 { - j = k - break - } - } - return Score(*r, i, j) -} - const CotacolAlgorithmVersion = "v1" func Cotacol(ride Ride) float64 { @@ -256,7 +241,7 @@ func difficultyScore(r Ride, startIndex, endIndex int) float64 { if lastDistance <= startDistance { return 0 } - score := 0.0 + cotacol := 0.0 i := startIndex for start := startDistance; start < lastDistance; start += 100 { end := math.Min(start+100, lastDistance) @@ -270,10 +255,10 @@ func difficultyScore(r Ride, startIndex, endIndex int) float64 { endElevation := interpolateElevation(r, i, end) slope := (endElevation - startElevation) / (end - start) if slope > 0 { - score += (end - start) / 1000 * (slope * 100) * (slope * 100) + cotacol += (end - start) / 1000 * (slope * 100) * (slope * 100) } } - return score + return cotacol } func interpolateElevation(r Ride, i int, distance float64) float64 { |