diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-27 11:21:17 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-08-27 11:21:17 +0200 |
| commit | 825cf388b55a6c8441e02149749bb6e2968477c0 (patch) | |
| tree | e3394cef97aa43df9775a1465d3c99f386ce3a9b /ride/ride.go | |
| parent | 366e7b0d671395c2ad32f85ee7706a592db69d15 (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/ride.go')
| -rw-r--r-- | ride/ride.go | 109 |
1 files changed, 14 insertions, 95 deletions
diff --git a/ride/ride.go b/ride/ride.go index 7863878..8e8bf95 100644 --- a/ride/ride.go +++ b/ride/ride.go @@ -4,13 +4,13 @@ import ( "errors" "fmt" "io" - "math" "os" "strconv" "strings" "time" "github.com/jftuga/geodist" + "github.com/martinlehoux/biking_home/climb" "github.com/martinlehoux/kagamigo/kcore" "github.com/tkrajina/gpxgo/gpx" ) @@ -20,9 +20,7 @@ const garminTrackPointExtensionNamespace = "http://www.garmin.com/xmlschemas/Tra var ErrNoTrackPoints = errors.New("ride has no track points") type Ride struct { - distances []float64 - elevations []float64 - coords []geodist.Coord + climb.Route timestamps []time.Time heartRates []float64 cadences []float64 @@ -31,26 +29,20 @@ type Ride struct { func (r *Ride) check() { kcore.Assert(r.Len() > 0, "no points in ride") - kcore.Assert(len(r.distances) == len(r.elevations), "ride columns have different lengths") - kcore.Assert(len(r.distances) == len(r.coords), "ride columns have different lengths") - kcore.Assert(len(r.distances) == len(r.timestamps), "ride columns have different lengths") + kcore.Assert(len(r.Distances) == len(r.Elevations), "ride columns have different lengths") + kcore.Assert(len(r.Distances) == len(r.Coords), "ride columns have different lengths") + kcore.Assert(len(r.Distances) == len(r.timestamps), "ride columns have different lengths") for _, column := range [][]float64{r.heartRates, r.cadences, r.powers} { if len(column) > 0 { - kcore.Assert(len(r.distances) == len(column), "ride columns have different lengths") + kcore.Assert(len(r.Distances) == len(column), "ride columns have different lengths") } } } func (r Ride) Len() int { - return len(r.distances) + return len(r.Distances) } -func (r Ride) Distances() []float64 { return r.distances } - -func (r Ride) Elevations() []float64 { return r.elevations } - -func (r Ride) Coords() []geodist.Coord { return r.coords } - func (r Ride) Timestamps() []time.Time { return r.timestamps } func (r Ride) Powers() ([]float64, bool) { return optionalComponent(r.powers) } @@ -170,9 +162,11 @@ func FromColumnsWithMetrics(distances []float64, elevations []float64, coords [] func fromColumns(distances []float64, elevations []float64, coords []geodist.Coord, timestamps []time.Time, heartRates, cadences, powers []float64) Ride { ride := Ride{ - distances: distances, - elevations: elevations, - coords: coords, + Route: climb.Route{ + Distances: distances, + Elevations: elevations, + Coords: coords, + }, timestamps: timestamps, heartRates: heartRates, cadences: cadences, @@ -229,81 +223,6 @@ func metricValue(point gpx.GPXPoint, name string) (*float64, error) { return &value, nil } -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.distances, ride.elevations, 0, ride.Len()-1) -} - -func difficultyScore(distances, elevations []float64, startIndex, endIndex int) float64 { - if endIndex-startIndex < 1 { - return 0 - } - startDistance := distances[startIndex] - lastDistance := distances[endIndex] - if lastDistance <= startDistance { - return 0 - } - cotacol := 0.0 - i := startIndex - for start := startDistance; start < lastDistance; start += cotacolSegmentLengthM { - end := math.Min(start+cotacolSegmentLengthM, lastDistance) - for i < endIndex && distances[i+1] <= start { - i++ - } - startElevation := interpolateElevation(distances, elevations, i, start) - for i < endIndex && distances[i+1] < end { - i++ - } - endElevation := interpolateElevation(distances, elevations, i, end) - slope := (endElevation - startElevation) / (end - start) - if slope > 0 { - cotacol += (end - start) / 1000 * (slope * 100) * (slope * 100) - } - } - return cotacol -} - -func interpolateElevation(distances, elevations []float64, i int, distance float64) float64 { - if i+1 >= len(distances) { - return elevations[i] - } - startDistance := distances[i] - endDistance := distances[i+1] - t := (distance - startDistance) / (endDistance - startDistance) - return elevations[i] + t*(elevations[i+1]-elevations[i]) -} - -func (r *Ride) ClimbFromDist(startDist, endDist float64) Climb { - start, end := 0, 0 - for i, distance := range r.distances { - if start == 0 && distance >= startDist { - start = i - } - if end == 0 && distance >= endDist { - end = i - break - } - } - return r.climbFrom(start, end) -} - -func (r *Ride) ClimbFromIndexes(startIndex, endIndex int) Climb { - kcore.Assert(startIndex >= 0 && endIndex < r.Len() && startIndex < endIndex, "invalid climb indexes") - return r.climbFrom(startIndex, endIndex) -} - -func (r *Ride) climbFrom(start, end int) Climb { - return Climb{ - distances: r.distances, - elevations: r.elevations, - coords: r.coords, - timestamps: r.timestamps, - rideStart: start, - rideEnd: end, - } +func (ride Ride) Cotacol() float64 { + return climb.Cotacol(ride.Distances, ride.Elevations) } |
