From 825cf388b55a6c8441e02149749bb6e2968477c0 Mon Sep 17 00:00:00 2001 From: Martin Kagamino Lehoux Date: Thu, 27 Aug 2026 11:21:17 +0200 Subject: 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. --- ride/detection_example_test.go | 87 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 ride/detection_example_test.go (limited to 'ride/detection_example_test.go') diff --git a/ride/detection_example_test.go b/ride/detection_example_test.go new file mode 100644 index 0000000..053abbb --- /dev/null +++ b/ride/detection_example_test.go @@ -0,0 +1,87 @@ +package ride_test + +import ( + "fmt" + "testing" + + "github.com/martinlehoux/biking_home/climb" + "github.com/martinlehoux/biking_home/ride" + "github.com/stretchr/testify/assert" +) + +var parser = ride.GPXRideParser{} + +// describeClimb renders a detected climb as the ride-style +// "start-end: length at slope (pts - cat)" string, so positions are asserted. +func describeClimb(route climb.Route, detected climb.DetectedClimb) string { + seg := detected.Climb + startKm := route.Distances[detected.Start] / 1000 + endKm := route.Distances[detected.Start+len(seg.Distances)-1] / 1000 + lengthKm := (seg.Distances[len(seg.Distances)-1] - seg.Distances[0]) / 1000 + slopePercent := climb.Slope(seg.Distances, seg.Elevations) * 100 + pts := int(seg.DifficultyScore()) + return fmt.Sprintf("%.1fkm-%.1fkm: %.1fkm at %.1f%% (%d pts - %s)", startKm, endKm, lengthKm, slopePercent, pts, climb.CategoryOf(seg.DifficultyScore())) +} + +func TestPogacar20220721(t *testing.T) { + r, err := ride.ParseFile(parser, "../examples/2022-07-21.Pogacar.gpx") + assert.NoError(t, err) + detected := climb.Detect(r.Route, nil, climb.ClimbSlopeMinimum) + + assert.Len(t, detected, 6) + assert.Equal(t, "0.6km-5.6km: 5.0km at 3.5% (92 pts - Cat 3)", describeClimb(r.Route, detected[0])) + assert.Equal(t, "42.9km-45.2km: 2.3km at 5.0% (74 pts - Cat 4)", describeClimb(r.Route, detected[1])) + assert.Equal(t, "60.0km-76.4km: 16.4km at 7.2% (977 pts - HC)", describeClimb(r.Route, detected[2])) + assert.Equal(t, "83.8km-85.9km: 2.1km at 5.4% (79 pts - Cat 4)", describeClimb(r.Route, detected[3])) + assert.Equal(t, "99.2km-109.3km: 10.2km at 8.4% (751 pts - HC)", describeClimb(r.Route, detected[4])) + assert.Equal(t, "128.6km-142.2km: 13.6km at 7.8% (930 pts - HC)", describeClimb(r.Route, detected[5])) +} + +func TestBouclesVerdon2024(t *testing.T) { + r, err := ride.ParseFile(parser, "../examples/2024-06-29.BouclesVerdon.gpx") + assert.NoError(t, err) + detected := climb.Detect(r.Route, nil, climb.ClimbSlopeMinimum) + + assert.Len(t, detected, 6) + assert.Equal(t, "6.8km-9.1km: 2.3km at 4.0% (41 pts - Cat 4)", describeClimb(r.Route, detected[0])) + assert.Equal(t, "34.7km-37.3km: 2.5km at 5.0% (67 pts - Cat 4)", describeClimb(r.Route, detected[1])) + assert.Equal(t, "41.9km-43.8km: 1.8km at 4.6% (39 pts - Cat 4)", describeClimb(r.Route, detected[2])) + assert.Equal(t, "57.6km-60.8km: 3.1km at 6.3% (139 pts - Cat 3)", describeClimb(r.Route, detected[3])) + assert.Equal(t, "71.7km-74.7km: 3.0km at 5.3% (90 pts - Cat 3)", describeClimb(r.Route, detected[4])) + assert.Equal(t, "77.4km-78.7km: 1.3km at 5.3% (39 pts - Cat 4)", describeClimb(r.Route, detected[5])) +} + +func TestMimetArbois20241229(t *testing.T) { + r, err := ride.ParseFile(parser, "../examples/2024-12-29.MimetArbois.gpx") + assert.NoError(t, err) + detected := climb.Detect(r.Route, nil, climb.ClimbSlopeMinimum) + + assert.Len(t, detected, 2) + assert.Equal(t, "3.9km-5.3km: 1.4km at 10.3% (165 pts - Cat 3)", describeClimb(r.Route, detected[0])) + assert.Equal(t, "14.4km-19.8km: 5.4km at 4.3% (113 pts - Cat 3)", describeClimb(r.Route, detected[1])) +} + +func TestMimetArbois20250104(t *testing.T) { + r, err := ride.ParseFile(parser, "../examples/2025-01-04.MimetArbois.gpx") + assert.NoError(t, err) + detected := climb.Detect(r.Route, nil, climb.ClimbSlopeMinimum) + + assert.Len(t, detected, 2) + assert.Equal(t, "4.5km-5.9km: 1.4km at 10.1% (159 pts - Cat 3)", describeClimb(r.Route, detected[0])) + assert.Equal(t, "12.0km-15.1km: 3.2km at 4.5% (99 pts - Cat 3)", describeClimb(r.Route, detected[1])) +} + +// 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) + detected := climb.Detect(r.Route, nil, climb.ClimbSlopeMinimum) + + assert.Len(t, detected, 6) + assert.Equal(t, "0.7km-4.6km: 3.9km at 4.2% (72 pts - Cat 4)", describeClimb(r.Route, detected[0])) + assert.Equal(t, "8.2km-18.6km: 10.3km at 4.4% (292 pts - Cat 1)", describeClimb(r.Route, detected[1])) + assert.Equal(t, "36.1km-41.6km: 5.4km at 5.8% (193 pts - Cat 2)", describeClimb(r.Route, detected[2])) + assert.Equal(t, "47.0km-48.2km: 1.2km at 6.1% (45 pts - Cat 4)", describeClimb(r.Route, detected[3])) + assert.Equal(t, "51.9km-54.7km: 2.8km at 4.0% (48 pts - Cat 4)", describeClimb(r.Route, detected[4])) + assert.Equal(t, "114.1km-114.7km: 0.6km at 14.3% (119 pts - Cat 3)", describeClimb(r.Route, detected[5])) +} -- cgit v1.3