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. --- rideanalysis/analysis.go | 49 +++++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 17 deletions(-) (limited to 'rideanalysis/analysis.go') diff --git a/rideanalysis/analysis.go b/rideanalysis/analysis.go index 5e08e65..a9cf93d 100644 --- a/rideanalysis/analysis.go +++ b/rideanalysis/analysis.go @@ -3,6 +3,8 @@ package rideanalysis import ( "slices" + "github.com/martinlehoux/biking_home/climb" + "github.com/martinlehoux/biking_home/core" "github.com/martinlehoux/biking_home/mountain_pass" "github.com/martinlehoux/biking_home/official_climb" "github.com/martinlehoux/biking_home/ride" @@ -21,7 +23,7 @@ type Result struct { } type AnalyzedClimb struct { - Segment ride.Climb + Segment climb.Climb Name string Category string DistanceM float64 @@ -29,6 +31,7 @@ type AnalyzedClimb struct { Cotacol float64 OfficialClimbID int64 OfficialName string + Interval core.Interval } func Analyze(parsed ride.Ride, passes []mountain_pass.MountainPass, officialClimbs []official_climb.OfficialClimb, matchPolicy official_climb.MatchPolicy) Result { @@ -37,45 +40,57 @@ func Analyze(parsed ride.Ride, passes []mountain_pass.MountainPass, officialClim Crossings: make([]mountain_pass.Crossing, 0), } claimedClimbs := make([]AnalyzedClimb, 0) - claimedRanges := make([]ride.Range, 0) + claimedRanges := make([]core.Interval, 0) for _, official := range officialClimbs { - for _, matched := range official_climb.MatchClimbs(parsed.Coords(), official, matchPolicy.EndpointRadiusM) { - claimedRanges = append(claimedRanges, ride.Range{Start: matched.StartIndex, End: matched.EndIndex}) - claimedClimbs = append(claimedClimbs, analyzedClimb(parsed, parsed.ClimbFromIndexes(matched.StartIndex, matched.EndIndex), official.Name, official.ID)) + for _, matched := range official_climb.MatchClimbs(parsed.Coords, official, matchPolicy.EndpointRadiusM) { + interval := core.NewInterval(matched.StartIndex, matched.EndIndex) + claimedRanges = append(claimedRanges, interval) + claimedClimbs = append(claimedClimbs, analyzedClimb(routeClimb(parsed.Route, interval), interval, official.Name, official.ID)) } } - slices.SortFunc(claimedRanges, func(a, b ride.Range) int { return a.Start - b.Start }) - for _, detectedClimb := range parsed.DetectClimbs(claimedRanges, ride.ClimbSlopeMinimum) { - displayClimb := detectedClimb - if matchedPass, ok := mountain_pass.MatchClimb(displayClimb, passes, passMatchRadiusM, passMatchElevationToleranceM); ok { - displayClimb.Name = matchedPass.Name + slices.SortFunc(claimedRanges, func(a, b core.Interval) int { return a.Start - b.Start }) + for _, detected := range climb.Detect(parsed.Route, claimedRanges, climb.ClimbSlopeMinimum) { + name := "" + if matchedPass, ok := mountain_pass.MatchClimb(detected.Climb, passes, passMatchRadiusM, passMatchElevationToleranceM); ok { + name = matchedPass.Name } - result.Climbs = append(result.Climbs, analyzedClimb(parsed, displayClimb, displayClimb.Name, 0)) + interval := core.NewInterval(detected.Start, detected.Start+len(detected.Climb.Distances)-1) + result.Climbs = append(result.Climbs, analyzedClimb(detected.Climb, interval, name, 0)) } climbs := make([]AnalyzedClimb, 0, len(claimedClimbs)+len(result.Climbs)) climbs = append(climbs, claimedClimbs...) climbs = append(climbs, result.Climbs...) slices.SortFunc(climbs, func(a, b AnalyzedClimb) int { - return a.Segment.StartIndex() - b.Segment.StartIndex() + return a.Interval.Start - b.Interval.Start }) result.Climbs = climbs - for _, crossing := range mountain_pass.DetectCrossings(parsed.Coords(), parsed.Distances(), parsed.Elevations(), passes, passCrossingRadiusM, passCrossingElevationToleranceM) { + for _, crossing := range mountain_pass.DetectCrossings(parsed.Coords, parsed.Distances, parsed.Elevations, passes, passCrossingRadiusM, passCrossingElevationToleranceM) { result.Crossings = append(result.Crossings, crossing) } return result } -func analyzedClimb(parsed ride.Ride, segment ride.Climb, name string, officialID int64) AnalyzedClimb { +// routeClimb returns the climb segment of route covered by interval. +func routeClimb(route climb.Route, interval core.Interval) climb.Climb { + return climb.Climb{Route: climb.Route{ + Distances: route.Distances[interval.Start : interval.End+1], + Elevations: route.Elevations[interval.Start : interval.End+1], + Coords: route.Coords[interval.Start : interval.End+1], + }} +} + +func analyzedClimb(segment climb.Climb, interval core.Interval, name string, officialID int64) AnalyzedClimb { cotacol := segment.DifficultyScore() return AnalyzedClimb{ Segment: segment, Name: name, - Category: ride.CategoryOf(cotacol).String(), - DistanceM: segment.EndDistanceM() - segment.StartDistanceM(), - SlopePercent: ride.Slope(parsed.Distances(), parsed.Elevations(), segment.StartIndex(), segment.EndIndex()) * 100, + Category: climb.CategoryOf(cotacol).String(), + DistanceM: segment.Distances[len(segment.Distances)-1] - segment.Distances[0], + SlopePercent: climb.Slope(segment.Distances, segment.Elevations) * 100, Cotacol: cotacol, OfficialClimbID: officialID, OfficialName: officialClimbNameForID(name, officialID), + Interval: interval, } } -- cgit v1.3