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 /climb/cotacol.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 'climb/cotacol.go')
| -rw-r--r-- | climb/cotacol.go | 48 |
1 files changed, 48 insertions, 0 deletions
diff --git a/climb/cotacol.go b/climb/cotacol.go new file mode 100644 index 0000000..85b096b --- /dev/null +++ b/climb/cotacol.go @@ -0,0 +1,48 @@ +package climb + +import "math" + +const ( + CotacolAlgorithmVersion = "v1" + cotacolSegmentLengthM = 100 +) + +func Cotacol(distances, elevations []float64) float64 { + startIndex, endIndex := 0, len(distances)-1 + 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]) +} |
