aboutsummaryrefslogtreecommitdiff
path: root/ride/climb.go
diff options
context:
space:
mode:
Diffstat (limited to 'ride/climb.go')
-rw-r--r--ride/climb.go126
1 files changed, 72 insertions, 54 deletions
diff --git a/ride/climb.go b/ride/climb.go
index 647cf9a..013104e 100644
--- a/ride/climb.go
+++ b/ride/climb.go
@@ -52,14 +52,17 @@ var categories = map[Category]categoryInfo{
const climbDetectionMinimumScore = 35
type Climb struct {
- ride Ride
- rideStart int
- rideEnd int
- Name string
+ distances []float64
+ elevations []float64
+ coords []geodist.Coord
+ timestamps []time.Time
+ rideStart int
+ rideEnd int
+ Name string
}
func (climb Climb) Duration() time.Duration {
- return climb.ride.Timestamp(climb.rideEnd).Sub(climb.ride.Timestamp(climb.rideStart))
+ return climb.timestamps[climb.rideEnd].Sub(climb.timestamps[climb.rideStart])
}
func (climb Climb) Speed() float64 {
@@ -71,31 +74,31 @@ func (climb Climb) StartIndex() int { return climb.rideStart }
func (climb Climb) EndIndex() int { return climb.rideEnd }
func (climb Climb) StartDistanceM() float64 {
- return climb.ride.DistanceM(climb.rideStart)
+ return climb.distances[climb.rideStart]
}
func (climb Climb) EndDistanceM() float64 {
- return climb.ride.DistanceM(climb.rideEnd)
+ return climb.distances[climb.rideEnd]
}
func (climb Climb) StartCoord() geodist.Coord {
- return climb.ride.Coord(climb.rideStart)
+ return climb.coords[climb.rideStart]
}
func (climb Climb) EndCoord() geodist.Coord {
- return climb.ride.Coord(climb.rideEnd)
+ return climb.coords[climb.rideEnd]
}
func (climb Climb) PointCoord(index int) geodist.Coord {
kcore.Assert(index >= climb.rideStart && index <= climb.rideEnd, "point is outside climb")
- return climb.ride.Coord(index)
+ return climb.coords[index]
}
// TopIndex returns the highest point inside the climb, used as the crest anchor.
func (climb Climb) TopIndex() int {
top := climb.rideStart
for i := climb.rideStart; i <= climb.rideEnd; i++ {
- if climb.ride.ElevationM(i) > climb.ride.ElevationM(top) {
+ if climb.elevations[i] > climb.elevations[top] {
top = i
}
}
@@ -103,22 +106,22 @@ func (climb Climb) TopIndex() int {
}
func (climb Climb) TopDistanceM() float64 {
- return climb.ride.DistanceM(climb.TopIndex())
+ return climb.distances[climb.TopIndex()]
}
func (climb Climb) TopElevationM() float64 {
- return climb.ride.ElevationM(climb.TopIndex())
+ return climb.elevations[climb.TopIndex()]
}
func (climb Climb) TopCoord() geodist.Coord {
- return climb.ride.Coord(climb.TopIndex())
+ return climb.coords[climb.TopIndex()]
}
func (climb Climb) String() string {
startDistance := climb.StartDistanceM()
endDistance := climb.EndDistanceM()
cotacol := climb.DifficultyScore()
- body := fmt.Sprintf("%.1fkm-%.1fkm: %.1fkm at %.1f%% (%d pts - %s)", startDistance/1000, endDistance/1000, (endDistance-startDistance)/1000, Slope(climb.ride, climb.rideStart, climb.rideEnd)*100, int(cotacol), CategoryOf(cotacol))
+ body := fmt.Sprintf("%.1fkm-%.1fkm: %.1fkm at %.1f%% (%d pts - %s)", startDistance/1000, endDistance/1000, (endDistance-startDistance)/1000, Slope(climb.distances, climb.elevations, climb.rideStart, climb.rideEnd)*100, int(cotacol), CategoryOf(cotacol))
if climb.Name == "" {
return body
}
@@ -126,20 +129,20 @@ func (climb Climb) String() string {
}
func (climb Climb) DifficultyScore() float64 {
- return difficultyScore(climb.ride, climb.rideStart, climb.rideEnd)
+ return difficultyScore(climb.distances, climb.elevations, climb.rideStart, climb.rideEnd)
}
-func Slope(r Ride, start, end int) float64 {
- return (r.ElevationM(end) - r.ElevationM(start)) / (r.DistanceM(end) - r.DistanceM(start))
+func Slope(distances, elevations []float64, start, end int) float64 {
+ return (elevations[end] - elevations[start]) / (distances[end] - distances[start])
}
-func climbDetectionScore(r Ride, start, end int) float64 {
+func climbDetectionScore(distances, elevations []float64, start, end int) float64 {
kcore.Assert(end > start, "no points for climb detection")
- distance := r.DistanceM(end) - r.DistanceM(start)
+ distance := distances[end] - distances[start]
if distance == 0 {
return 0
}
- dElevation := r.ElevationM(end) - r.ElevationM(start)
+ dElevation := elevations[end] - elevations[start]
return math.Abs(dElevation) * dElevation / distance * 100.0 * 100.0 / 1000.0
}
@@ -167,16 +170,21 @@ func CategoryOf(cotacol float64) Category {
return best
}
+type interval struct {
+ start int
+ end int
+}
+
// bestClimbBetween returns the highest-scoring climb in [start, end]; when
// that climb averages below minSlope, it instead returns the best qualifying
// sub-segment (found=false when none exists).
-func bestClimbBetween(r Ride, start, end int, minSlope float64) (Climb, bool) {
+func bestClimbBetween(distances, elevations []float64, start, end int, minSlope float64) (interval, bool) {
kcore.Assert(end > start, "empty points")
- bestDetectionScore := climbDetectionScore(r, start, end)
+ bestDetectionScore := climbDetectionScore(distances, elevations, start, end)
bestStart := start
for i := start; i < end; i++ {
- detectionScore := climbDetectionScore(r, i, end)
+ detectionScore := climbDetectionScore(distances, elevations, i, end)
if detectionScore > bestDetectionScore {
bestStart = i
bestDetectionScore = detectionScore
@@ -184,32 +192,32 @@ func bestClimbBetween(r Ride, start, end int, minSlope float64) (Climb, bool) {
}
bestEnd := end
for i := end; i > bestStart; i-- {
- detectionScore := climbDetectionScore(r, bestStart, i)
+ detectionScore := climbDetectionScore(distances, elevations, bestStart, i)
if detectionScore > bestDetectionScore {
bestEnd = i
bestDetectionScore = detectionScore
}
}
for i := bestStart; i < bestEnd; i++ {
- detectionScore := climbDetectionScore(r, i, bestEnd)
+ detectionScore := climbDetectionScore(distances, elevations, i, bestEnd)
if detectionScore > bestDetectionScore {
bestStart = i
bestDetectionScore = detectionScore
}
}
kcore.Assert(bestStart < bestEnd, "empty climb")
- if minSlope <= 0 || slopeBetween(r, bestStart, bestEnd) >= minSlope {
- return Climb{ride: r, rideStart: bestStart, rideEnd: bestEnd}, true
+ if minSlope <= 0 || Slope(distances, elevations, bestStart, bestEnd) >= minSlope {
+ return interval{bestStart, bestEnd}, true
}
// Classic best too gentle: re-segment into the best qualifying sub-segment.
bestDetectionScore = math.Inf(-1)
bestStart, bestEnd = -1, -1
for i := start; i < end; i++ {
for j := i + 1; j <= end; j++ {
- if slopeBetween(r, i, j) < minSlope {
+ if Slope(distances, elevations, i, j) < minSlope {
continue
}
- detectionScore := climbDetectionScore(r, i, j)
+ detectionScore := climbDetectionScore(distances, elevations, i, j)
if detectionScore > bestDetectionScore {
bestDetectionScore = detectionScore
bestStart, bestEnd = i, j
@@ -217,43 +225,40 @@ func bestClimbBetween(r Ride, start, end int, minSlope float64) (Climb, bool) {
}
}
if bestStart < 0 {
- return Climb{}, false
+ return interval{}, false
}
- return Climb{ride: r, rideStart: bestStart, rideEnd: bestEnd}, true
-}
-
-// slopeBetween returns the average slope between two indexes.
-func slopeBetween(r Ride, start, end int) float64 {
- return (r.ElevationM(end) - r.ElevationM(start)) / (r.DistanceM(end) - r.DistanceM(start))
+ return interval{bestStart, bestEnd}, true
}
-func climbsBetween(r Ride, start, end int, minSlope float64) []Climb {
- climbs := []Climb{}
- if r.DistanceM(end)-r.DistanceM(start) < ClimbDistanceMinimum {
+// climbsBetween returns the detected climb bounds in [start, end] from the
+// distance and elevation components.
+func climbsBetween(distances, elevations []float64, start, end int, minSlope float64) []interval {
+ climbs := []interval{}
+ if distances[end]-distances[start] < ClimbDistanceMinimum {
return climbs
}
- slog.Debug("Searching climbs between", slog.Int("start", int(r.DistanceM(start))), slog.Int("end", int(r.DistanceM(end))))
+ slog.Debug("Searching climbs between", slog.Int("start", int(distances[start])), slog.Int("end", int(distances[end])))
highest := start
for i := start; i <= end; i++ {
- if r.ElevationM(i) > r.ElevationM(highest) {
+ if elevations[i] > elevations[highest] {
highest = i
}
}
// TODO: Use descent to reduce recursion
- if r.DistanceM(highest)-r.DistanceM(start) < ClimbDistanceMinimum {
- return climbsBetween(r, start+1, end, minSlope)
+ if distances[highest]-distances[start] < ClimbDistanceMinimum {
+ return climbsBetween(distances, elevations, start+1, end, minSlope)
}
- climb, found := bestClimbBetween(r, start, highest, minSlope)
+ climb, found := bestClimbBetween(distances, elevations, start, highest, minSlope)
if !found {
// No qualifying climb up to the top; only the part past it can hold climbs.
- return climbsBetween(r, highest, end, minSlope)
+ return climbsBetween(distances, elevations, highest, end, minSlope)
}
- if climbDetectionScore(r, climb.rideStart, climb.rideEnd) >= climbDetectionMinimumScore && climb.EndDistanceM()-climb.StartDistanceM() >= ClimbDistanceMinimum {
- slog.Debug("Found climb between", slog.Int("start", int(climb.StartDistanceM())), slog.Int("end", int(climb.EndDistanceM())))
+ if climbDetectionScore(distances, elevations, climb.start, climb.end) >= climbDetectionMinimumScore && distances[climb.end]-distances[climb.start] >= ClimbDistanceMinimum {
+ slog.Debug("Found climb between", slog.Int("start", int(distances[climb.start])), slog.Int("end", int(distances[climb.end])))
climbs = append(climbs, climb)
}
- climbs = append(climbs, climbsBetween(r, start, climb.rideStart, minSlope)...)
- climbs = append(climbs, climbsBetween(r, climb.rideEnd, end, minSlope)...)
+ climbs = append(climbs, climbsBetween(distances, elevations, start, climb.start, minSlope)...)
+ climbs = append(climbs, climbsBetween(distances, elevations, climb.end, end, minSlope)...)
return climbs
}
@@ -300,12 +305,25 @@ func freeRegions(rStart, rEnd int, claimed []Range) []Range {
// returns the detected climbs of the whole ride, but never a climb that overlaps
// a claimed range.
func (ride *Ride) DetectClimbs(claimed []Range, minSlope float64) []Climb {
- climbs := make([]Climb, 0)
+ bounds := make([]interval, 0)
for _, region := range freeRegions(0, ride.Len()-1, claimed) {
- climbs = append(climbs, climbsBetween(*ride, region.Start, region.End, minSlope)...)
+ bounds = append(bounds, climbsBetween(ride.distances, ride.elevations, region.Start, region.End, minSlope)...)
+ }
+ slices.SortFunc(bounds, func(a, b interval) int { return a.start - b.start })
+ climbs := make([]Climb, 0, len(bounds))
+ for _, b := range bounds {
+ climbs = append(climbs, climbFromBounds(ride, b))
}
- slices.SortFunc(climbs, climbCmpStart)
return climbs
}
-func climbCmpStart(a, b Climb) int { return a.rideStart - b.rideStart }
+func climbFromBounds(ride *Ride, b interval) Climb {
+ return Climb{
+ distances: ride.distances,
+ elevations: ride.elevations,
+ coords: ride.coords,
+ timestamps: ride.timestamps,
+ rideStart: b.start,
+ rideEnd: b.end,
+ }
+}