diff options
Diffstat (limited to 'ride/climb.go')
| -rw-r--r-- | ride/climb.go | 126 |
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, + } +} |
