summaryrefslogtreecommitdiff
path: root/ride/climb.go
blob: a337a155090dd08ee898a2e2c8e7484e5fddfde0 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package ride

import (
	"fmt"
	"math"
	"slices"

	"github.com/martinlehoux/kagamigo/kcore"
)

const ClimbDistanceMinimum = 500

type Climb struct {
	start int
	end   int
}

type Point struct {
	distance  float64
	elevation float64
}

func NewPoint(distance float64, elevation float64) Point {
	return Point{
		distance:  distance,
		elevation: elevation,
	}
}

func Slope(start, end Point) float64 {
	return (end.elevation - start.elevation) / (end.distance - start.distance)
}

func Score(points []Point, start int, end int) float64 {
	kcore.Assert(end > start, "no points for score")
	distance := points[end].distance - points[start].distance
	if distance == 0 {
		return 0
	}
	dElevation := points[end].elevation - points[start].elevation

	return math.Abs(dElevation) * dElevation / distance * 100.0 * 100.0 / 1000.0
}

func Category(score float64) string {
	switch {
	case score < 35:
		return "NO"
	case score < 80:
		return "Cat 4"
	case score < 180:
		return "Cat 3"
	case score < 250:
		return "Cat 2"
	case score < 600:
		return "Cat 1"
	default:
		return "HC"
	}
}

func bestClimbUntilEnd(points []Point, start int, end int) Climb {
	kcore.Assert(end > start, "empty points")

	bestScore := Score(points, start, end)
	bestStart := start
	for i := start; i < end; i++ {
		score := Score(points, i, end)
		if score > bestScore {
			bestStart = i
			bestScore = score
		}
	}
	climb := Climb{bestStart, end}

	kcore.Assert(climb.start < climb.end, "empty climb")
	return climb
}

func climbsBetween(points []Point, start int, end int) []Climb {
	climbs := []Climb{}
	if points[end].distance-points[start].distance < ClimbDistanceMinimum {
		return climbs
	}
	fmt.Printf("Searching climbs between %.1fkm and %.1fkm\n", points[start].distance/1000, points[end].distance/1000)
	highest := start
	for i := start; i < end; i++ {
		if points[i].elevation > points[highest].elevation {
			highest = i
		}
	}
	// TODO: Use descent to reduce recursion
	if highest == start {
		return climbsBetween(points, start+1, end)
	}
	climb := bestClimbUntilEnd(points, start, highest)
	if Score(points, climb.start, climb.end) >= 35 {
		fmt.Printf("Found climb between %.1fkm and %.1fkm\n", points[climb.start].distance/1000, points[climb.end].distance/1000)
		climbs = append(climbs, climb)
	}
	climbs = append(climbs, climbsBetween(points, start, climb.start)...)
	climbs = append(climbs, climbsBetween(points, climb.end, end)...)

	return climbs
}

func (ride *Ride) AllClimbs() []Climb {
	climbs := climbsBetween(ride.points, 0, len(ride.points)-1)
	slices.SortFunc(climbs, climbCmpStart)
	return climbs
}

func climbCmpStart(a, b Climb) int { return a.start - b.start }