aboutsummaryrefslogtreecommitdiff
path: root/ride/climb_test.go
diff options
context:
space:
mode:
authorMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 11:21:17 +0200
committerMartin Kagamino Lehoux <martin@lehoux.net>2026-08-27 11:21:17 +0200
commit825cf388b55a6c8441e02149749bb6e2968477c0 (patch)
treee3394cef97aa43df9775a1465d3c99f386ce3a9b /ride/climb_test.go
parent366e7b0d671395c2ad32f85ee7706a592db69d15 (diff)
Extract climb detection into a dedicated climb package
Move climb detection, slope, and Cotacol scoring out of ride into a new climb package that operates on a geometry-only Route (distances, elevations, coords). Ride embeds climb.Route and keeps its timing and telemetry columns. - climb.Detect returns []DetectedClimb (Climb + ride-absolute Start). - Climb owns its sliced geometry; indices are local, with Start reported at the detection boundary for absolute position. - Detected bounds use core.Interval; freeRegions claim suppression fixed. - ride no longer builds slices: ClimbFromDist/ClimbFromIndexes/climbFrom and ride.Climb/Interval/Range are removed; rideanalysis, mountain_pass MatchClimb, and web views are rewired to climb types. - New internal/routetest builds climb.Route from the ride DSL; ridetest reuses it and adds timestamps and telemetry. - Tests moved to climb (synthetic detection and difficulty) and kept in ride for GPX-example integration; ride.comparison detection coverage retained. - mountain_pass.nearestCrossing now reuses geo.Nearest.
Diffstat (limited to 'ride/climb_test.go')
-rw-r--r--ride/climb_test.go135
1 files changed, 0 insertions, 135 deletions
diff --git a/ride/climb_test.go b/ride/climb_test.go
deleted file mode 100644
index 3632da7..0000000
--- a/ride/climb_test.go
+++ /dev/null
@@ -1,135 +0,0 @@
-package ride_test
-
-import (
- "testing"
-
- "github.com/martinlehoux/biking_home/internal/ridetest"
- "github.com/martinlehoux/biking_home/ride"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-var parser = ride.GPXRideParser{}
-
-func TestClimbThenFalseFlat(t *testing.T) {
- r := ridetest.MustRide("2km at 7% | 10km at 1%")
- climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
-
- assert.Len(t, climbs, 1)
- assert.Equal(t, "0.0km-2.0km: 2.0km at 7.0% (98 pts - Cat 3)", climbs[0].String())
-}
-
-func TestSmallClimbWithDescentInsideFalseFlat(t *testing.T) {
- r := ridetest.MustRide("10km at 1% | 2km at 7% | 1km at -7% | 10km at 1%")
- climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
-
- assert.Len(t, climbs, 1)
- assert.Equal(t, "10.0km-12.0km: 2.0km at 7.0% (98 pts - Cat 3)", climbs[0].String())
-}
-
-func TestPogacar20220721(t *testing.T) {
- r, err := ride.ParseFile(parser, "../examples/2022-07-21.Pogacar.gpx")
- assert.NoError(t, err)
- climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
-
- assert.Len(t, climbs, 6)
- 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) {
- r, err := ride.ParseFile(parser, "../examples/2024-06-29.BouclesVerdon.gpx")
- assert.NoError(t, err)
- climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
-
- assert.Len(t, climbs, 6)
- assert.Equal(t, "6.8km-9.1km: 2.3km at 4.0% (41 pts - Cat 4)", climbs[0].String())
- assert.Equal(t, "34.7km-37.3km: 2.5km at 5.0% (67 pts - Cat 4)", climbs[1].String())
- assert.Equal(t, "41.9km-43.8km: 1.8km at 4.6% (39 pts - Cat 4)", climbs[2].String())
- 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) {
- r, err := ride.ParseFile(parser, "../examples/2024-12-29.MimetArbois.gpx")
- assert.NoError(t, err)
- climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
-
- assert.Len(t, climbs, 2)
- 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) {
- r, err := ride.ParseFile(parser, "../examples/2025-01-04.MimetArbois.gpx")
- assert.NoError(t, err)
- climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
-
- assert.Len(t, climbs, 2)
- assert.Equal(t, "4.5km-5.9km: 1.4km at 10.1% (159 pts - Cat 3)", climbs[0].String())
- assert.Equal(t, "12.0km-15.1km: 3.2km at 4.5% (99 pts - Cat 3)", climbs[1].String())
-}
-
-// https://www.strava.com/activities/9282265844
-func TestAlpesVerdonTour20230617(t *testing.T) {
- r, err := ride.ParseFile(parser, "../examples/2023-06-17.AlpesVerdonTour.gpx")
- assert.NoError(t, err)
- climbs := r.DetectClimbs(nil, ride.ClimbSlopeMinimum)
-
- assert.Len(t, climbs, 6)
- assert.Equal(t, "0.7km-4.6km: 3.9km at 4.2% (72 pts - Cat 4)", climbs[0].String())
- assert.Equal(t, "8.2km-18.6km: 10.3km at 4.4% (292 pts - Cat 1)", climbs[1].String())
- assert.Equal(t, "36.1km-41.6km: 5.4km at 5.8% (193 pts - Cat 2)", climbs[2].String())
- assert.Equal(t, "47.0km-48.2km: 1.2km at 6.1% (45 pts - Cat 4)", climbs[3].String())
- assert.Equal(t, "51.9km-54.7km: 2.8km at 4.0% (48 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())
-}
-
-func TestDetectClimbsSkipsMiddleClaim(t *testing.T) {
- // A climb, a false flat giving a saddle, then a climb again. Claiming the
- // saddle's descent base should leave both climbs detected separately.
- r := ridetest.MustRide("2km at 7% | 1km at -7% | 2km at 7%")
- climbs := r.DetectClimbs([]ride.Range{{Start: 20, End: 20}}, ride.ClimbSlopeMinimum)
-
- // No free region falls under the min distance except the two climbs.
- assert.Len(t, climbs, 2)
- assert.Equal(t, "0.0km-2.0km: 2.0km at 7.0% (98 pts - Cat 3)", climbs[0].String())
- assert.Equal(t, "3.0km-5.0km: 2.0km at 7.0% (98 pts - Cat 3)", climbs[1].String())
-}
-
-func TestDetectClimbsOverlappingClaimDropsClimb(t *testing.T) {
- // Claiming the ascent region entirely swallows the climb.
- r := ridetest.MustRide("2km at 7% | 20km at 0%")
- climbs := r.DetectClimbs([]ride.Range{{Start: 0, End: 40}}, ride.ClimbSlopeMinimum)
- assert.Empty(t, climbs)
-}
-
-func TestDetectClimbsIgnoresOutOfRangeClaim(t *testing.T) {
- r := ridetest.MustRide("2km at 7% | 20km at 0%")
- climbs := r.DetectClimbs([]ride.Range{{Start: -5, End: -1}}, ride.ClimbSlopeMinimum)
- require.Len(t, climbs, 1)
- assert.Equal(t, "0.0km-2.0km: 2.0km at 7.0% (98 pts - Cat 3)", climbs[0].String())
-}
-
-func TestDetectClimbsWithUnsortedClaimsDropsOverlappingClimb(t *testing.T) {
- // Reproduces ride 6 Chemin des Lauriers bug: DetectClimbs must not depend on
- // caller-provided order. Two official claims out of ID order (Bastides
- // 2140-2627 before Lauriers 1215-1588) caused freeRegions to skip the
- // earlier claim and leave the overlapping Cat 4 1352-1601 visible.
- // Minimal reproduction: two climbs 0-2km and 3-5km separated by a descent.
- r := ridetest.MustRide("2km at 7% | 1km at -7% | 2km at 7%")
- // Both climbs claimed, but in reverse order (mimics ORDER BY id returning 30-50 before 0-20).
- // Sorted input correctly yields 0 detected climbs.
- sorted := []ride.Range{{Start: 0, End: 20}, {Start: 30, End: 50}}
- assert.Empty(t, r.DetectClimbs(sorted, ride.ClimbSlopeMinimum))
-
- unsorted := []ride.Range{{Start: 30, End: 50}, {Start: 0, End: 20}}
- climbs := r.DetectClimbs(unsorted, ride.ClimbSlopeMinimum)
- // Before fix this fails: freeRegions skips {0,20} because 20 < 30 and returns 1 climb.
- assert.Empty(t, climbs, "unsorted claimed ranges must still suppress overlapping climbs — expected 0, got %v", climbs)
-}