aboutsummaryrefslogtreecommitdiff
path: root/climb/cotacol.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 /climb/cotacol.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 'climb/cotacol.go')
-rw-r--r--climb/cotacol.go48
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])
+}