aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--official_climb/official_climb_test.go18
-rw-r--r--ride/climb.go126
-rw-r--r--ride/ecs_test.go43
-rw-r--r--ride/moving_time.go18
-rw-r--r--ride/ride.go72
-rw-r--r--rideanalysis/analysis.go4
6 files changed, 187 insertions, 94 deletions
diff --git a/official_climb/official_climb_test.go b/official_climb/official_climb_test.go
index a0c8d65..266581e 100644
--- a/official_climb/official_climb_test.go
+++ b/official_climb/official_climb_test.go
@@ -68,7 +68,7 @@ func TestMatchClimbMatchesWithoutDetectedClimb(t *testing.T) {
StartCoord: parsed.Coord(1),
EndCoord: parsed.Coord(2),
}
- matched, found := official_climb.MatchClimb(parsed.Coords, official, official_climb.DefaultMatchRadiusM)
+ matched, found := official_climb.MatchClimb(parsed.Coords(), official, official_climb.DefaultMatchRadiusM)
require.True(t, found)
assert.Equal(t, 1, matched.StartIndex)
assert.Equal(t, 2, matched.EndIndex)
@@ -91,7 +91,7 @@ func TestMatchClimbMatchesEndpointInsideRadius(t *testing.T) {
StartCoord: geodist.Coord{Lat: 43.1101, Lon: 5.1101},
EndCoord: geodist.Coord{Lat: 43.1201, Lon: 5.1201},
}
- matched, found := official_climb.MatchClimb(parsed.Coords, official, official_climb.DefaultMatchRadiusM)
+ matched, found := official_climb.MatchClimb(parsed.Coords(), official, official_climb.DefaultMatchRadiusM)
require.True(t, found)
assert.Equal(t, 1, matched.StartIndex)
assert.Equal(t, 2, matched.EndIndex)
@@ -114,7 +114,7 @@ func TestMatchClimbRejectsReversedEndpoints(t *testing.T) {
StartCoord: parsed.Coord(2),
EndCoord: parsed.Coord(1),
}
- _, found := official_climb.MatchClimb(parsed.Coords, official, official_climb.DefaultMatchRadiusM)
+ _, found := official_climb.MatchClimb(parsed.Coords(), official, official_climb.DefaultMatchRadiusM)
assert.False(t, found)
}
@@ -135,7 +135,7 @@ func TestMatchClimbRejectsEndpointOutsideRadius(t *testing.T) {
StartCoord: geodist.Coord{Lat: 43.09, Lon: 5.09},
EndCoord: parsed.Coord(2),
}
- _, found := official_climb.MatchClimb(parsed.Coords, official, 10)
+ _, found := official_climb.MatchClimb(parsed.Coords(), official, 10)
assert.False(t, found)
}
@@ -161,14 +161,14 @@ func TestMatchClimbsMatchesTwoLaps(t *testing.T) {
StartCoord: geodist.Coord{Lat: 43.10, Lon: 5.10},
EndCoord: geodist.Coord{Lat: 43.12, Lon: 5.12},
}
- matches := official_climb.MatchClimbs(parsed.Coords, official, official_climb.DefaultMatchRadiusM)
+ matches := official_climb.MatchClimbs(parsed.Coords(), official, official_climb.DefaultMatchRadiusM)
require.Len(t, matches, 2)
assert.Equal(t, 0, matches[0].StartIndex)
assert.Equal(t, 2, matches[0].EndIndex)
assert.Equal(t, 4, matches[1].StartIndex)
assert.Equal(t, 6, matches[1].EndIndex)
// Single-match helper still returns first occurrence for backward compatibility.
- single, found := official_climb.MatchClimb(parsed.Coords, official, official_climb.DefaultMatchRadiusM)
+ single, found := official_climb.MatchClimb(parsed.Coords(), official, official_climb.DefaultMatchRadiusM)
require.True(t, found)
assert.Equal(t, 0, single.StartIndex)
}
@@ -193,7 +193,7 @@ func TestMatchClimbsMatchesOnlyCompleteLaps(t *testing.T) {
StartCoord: geodist.Coord{Lat: 43.10, Lon: 5.10},
EndCoord: geodist.Coord{Lat: 43.12, Lon: 5.12},
}
- matches := official_climb.MatchClimbs(parsed.Coords, official, official_climb.DefaultMatchRadiusM)
+ matches := official_climb.MatchClimbs(parsed.Coords(), official, official_climb.DefaultMatchRadiusM)
require.Len(t, matches, 1)
assert.Equal(t, 3, matches[0].StartIndex)
assert.Equal(t, 5, matches[0].EndIndex)
@@ -216,7 +216,7 @@ func TestMatchClimbsPreservesDirectionAcrossLaps(t *testing.T) {
StartCoord: geodist.Coord{Lat: 43.10, Lon: 5.10},
EndCoord: geodist.Coord{Lat: 43.12, Lon: 5.12},
}
- matches := official_climb.MatchClimbs(parsed.Coords, official, official_climb.DefaultMatchRadiusM)
+ matches := official_climb.MatchClimbs(parsed.Coords(), official, official_climb.DefaultMatchRadiusM)
assert.Empty(t, matches)
}
@@ -242,7 +242,7 @@ func TestMatchClimbsClustersContiguousPoints(t *testing.T) {
StartCoord: geodist.Coord{Lat: 43.10, Lon: 5.10},
EndCoord: geodist.Coord{Lat: 43.12, Lon: 5.12},
}
- matches := official_climb.MatchClimbs(parsed.Coords, official, official_climb.DefaultMatchRadiusM)
+ matches := official_climb.MatchClimbs(parsed.Coords(), official, official_climb.DefaultMatchRadiusM)
require.Len(t, matches, 2)
assert.Equal(t, 0, matches[0].StartIndex)
assert.Equal(t, 4, matches[0].EndIndex)
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,
+ }
+}
diff --git a/ride/ecs_test.go b/ride/ecs_test.go
new file mode 100644
index 0000000..01c0628
--- /dev/null
+++ b/ride/ecs_test.go
@@ -0,0 +1,43 @@
+package ride
+
+import (
+ "testing"
+ "time"
+
+ "github.com/jftuga/geodist"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRideExposesComponentGetters(t *testing.T) {
+ distances := []float64{0, 500, 1000}
+ elevations := []float64{0, 35, 70}
+ coords := []geodist.Coord{{Lat: 43.0, Lon: 5.0}, {Lat: 43.1, Lon: 5.1}, {Lat: 43.2, Lon: 5.2}}
+ timestamps := []time.Time{{}, time.Time{}.Add(time.Second), time.Time{}.Add(2 * time.Second)}
+
+ r := FromColumns(distances, elevations, coords, timestamps)
+
+ assert.Equal(t, distances, r.Distances())
+ assert.Equal(t, elevations, r.Elevations())
+ assert.Equal(t, coords, r.Coords())
+ assert.Equal(t, timestamps, r.Timestamps())
+
+ powers, ok := r.Powers()
+ assert.False(t, ok, "no power component present")
+ assert.Nil(t, powers)
+ cadences, ok := r.Cadences()
+ assert.False(t, ok, "no cadence component present")
+ assert.Nil(t, cadences)
+}
+
+func TestDifficultyScoreReadsOnlyDistanceAndElevationComponents(t *testing.T) {
+ // A 1 km climb at a steady 7%, sampled every 100 m.
+ distances := make([]float64, 0, 11)
+ elevations := make([]float64, 0, 11)
+ for i := 0; i <= 10; i++ {
+ distances = append(distances, float64(i*100))
+ elevations = append(elevations, float64(i*7))
+ }
+
+ // Reference Cotacol value: 10 segments of 100 m at 7% => 49 pts.
+ assert.InDelta(t, 49, difficultyScore(distances, elevations, 0, len(distances)-1), 0.5)
+}
diff --git a/ride/moving_time.go b/ride/moving_time.go
index 84cc28e..2ab07bf 100644
--- a/ride/moving_time.go
+++ b/ride/moving_time.go
@@ -19,25 +19,25 @@ const movingTimeMaxGap = 120 * time.Second
// Bench (104 rides, `-backtest`, distance >= 10 km, moving/elapsed ratio >= 0.8):
// absMean 0.4%, stderr 0.1, p50 0.2, p75 0.4, p90 0.8, p95 1.4, p99 2.9.
func MovingTime(r Ride) time.Duration {
- return movingTime(r, movingTimeMinSpeedMps, movingTimeMaxGap)
+ return movingTime(r.timestamps, r.distances, r.powers, r.cadences, movingTimeMinSpeedMps, movingTimeMaxGap)
}
-func movingTime(r Ride, minSpeedMps float64, maxGap time.Duration) time.Duration {
+func movingTime(timestamps []time.Time, distances, powers, cadences []float64, minSpeedMps float64, maxGap time.Duration) time.Duration {
var moving time.Duration
- for i := 1; i < r.Len(); i++ {
- dt := r.Timestamp(i).Sub(r.Timestamp(i - 1))
+ for i := 1; i < len(timestamps); i++ {
+ dt := timestamps[i].Sub(timestamps[i-1])
if dt <= 0 || dt > maxGap {
continue
}
- dd := r.DistanceM(i) - r.DistanceM(i-1)
+ dd := distances[i] - distances[i-1]
gpsMoving := dd/dt.Seconds() > minSpeedMps
powerMoving := false
- if power, ok := r.PowerW(i); ok {
- powerMoving = power >= movingTimeMinPowerW
+ if len(powers) > 0 {
+ powerMoving = powers[i] >= movingTimeMinPowerW
}
cadenceMoving := false
- if cadence, ok := r.CadenceRpm(i); ok {
- cadenceMoving = cadence > 0
+ if len(cadences) > 0 {
+ cadenceMoving = cadences[i] > 0
}
if gpsMoving || powerMoving || cadenceMoving {
moving += dt
diff --git a/ride/ride.go b/ride/ride.go
index d5533ae..a694df9 100644
--- a/ride/ride.go
+++ b/ride/ride.go
@@ -22,7 +22,7 @@ var ErrNoTrackPoints = errors.New("ride has no track points")
type Ride struct {
distances []float64
elevations []float64
- Coords []geodist.Coord
+ coords []geodist.Coord
timestamps []time.Time
heartRates []float64
cadences []float64
@@ -32,7 +32,7 @@ type Ride struct {
func (r *Ride) check() {
kcore.Assert(r.Len() > 0, "no points in ride")
kcore.Assert(len(r.distances) == len(r.elevations), "ride columns have different lengths")
- kcore.Assert(len(r.distances) == len(r.Coords), "ride columns have different lengths")
+ kcore.Assert(len(r.distances) == len(r.coords), "ride columns have different lengths")
kcore.Assert(len(r.distances) == len(r.timestamps), "ride columns have different lengths")
for _, column := range [][]float64{r.heartRates, r.cadences, r.powers} {
if len(column) > 0 {
@@ -45,11 +45,25 @@ func (r Ride) Len() int {
return len(r.distances)
}
+func (r Ride) Distances() []float64 { return r.distances }
+
+func (r Ride) Elevations() []float64 { return r.elevations }
+
+func (r Ride) Coords() []geodist.Coord { return r.coords }
+
+func (r Ride) Timestamps() []time.Time { return r.timestamps }
+
+func (r Ride) Powers() ([]float64, bool) { return optionalComponent(r.powers) }
+
+func (r Ride) Cadences() ([]float64, bool) { return optionalComponent(r.cadences) }
+
+func (r Ride) HeartRates() ([]float64, bool) { return optionalComponent(r.heartRates) }
+
func (r Ride) DistanceM(i int) float64 { return r.distances[i] }
func (r Ride) ElevationM(i int) float64 { return r.elevations[i] }
-func (r Ride) Coord(i int) geodist.Coord { return r.Coords[i] }
+func (r Ride) Coord(i int) geodist.Coord { return r.coords[i] }
func (r Ride) Timestamp(i int) time.Time { return r.timestamps[i] }
@@ -65,6 +79,13 @@ func (r Ride) PowerW(i int) (float64, bool) {
return optionalMetric(r.powers, i)
}
+func optionalComponent(column []float64) ([]float64, bool) {
+ if len(column) == 0 {
+ return nil, false
+ }
+ return column, true
+}
+
func optionalMetric(column []float64, i int) (float64, bool) {
if len(column) == 0 {
return 0, false
@@ -171,7 +192,7 @@ func fromColumns(distances []float64, elevations []float64, coords []geodist.Coo
ride := Ride{
distances: distances,
elevations: elevations,
- Coords: coords,
+ coords: coords,
timestamps: timestamps,
heartRates: heartRates,
cadences: cadences,
@@ -235,15 +256,15 @@ const CotacolAlgorithmVersion = "v1"
const cotacolSegmentLengthM = 100
func Cotacol(ride Ride) float64 {
- return difficultyScore(ride, 0, ride.Len()-1)
+ return difficultyScore(ride.distances, ride.elevations, 0, ride.Len()-1)
}
-func difficultyScore(r Ride, startIndex, endIndex int) float64 {
+func difficultyScore(distances, elevations []float64, startIndex, endIndex int) float64 {
if endIndex-startIndex < 1 {
return 0
}
- startDistance := r.DistanceM(startIndex)
- lastDistance := r.DistanceM(endIndex)
+ startDistance := distances[startIndex]
+ lastDistance := distances[endIndex]
if lastDistance <= startDistance {
return 0
}
@@ -251,14 +272,14 @@ func difficultyScore(r Ride, startIndex, endIndex int) float64 {
i := startIndex
for start := startDistance; start < lastDistance; start += cotacolSegmentLengthM {
end := math.Min(start+cotacolSegmentLengthM, lastDistance)
- for i < endIndex && r.DistanceM(i+1) <= start {
+ for i < endIndex && distances[i+1] <= start {
i++
}
- startElevation := interpolateElevation(r, i, start)
- for i < endIndex && r.DistanceM(i+1) < end {
+ startElevation := interpolateElevation(distances, elevations, i, start)
+ for i < endIndex && distances[i+1] < end {
i++
}
- endElevation := interpolateElevation(r, i, end)
+ endElevation := interpolateElevation(distances, elevations, i, end)
slope := (endElevation - startElevation) / (end - start)
if slope > 0 {
cotacol += (end - start) / 1000 * (slope * 100) * (slope * 100)
@@ -267,14 +288,14 @@ func difficultyScore(r Ride, startIndex, endIndex int) float64 {
return cotacol
}
-func interpolateElevation(r Ride, i int, distance float64) float64 {
- if i+1 >= r.Len() {
- return r.ElevationM(i)
+func interpolateElevation(distances, elevations []float64, i int, distance float64) float64 {
+ if i+1 >= len(distances) {
+ return elevations[i]
}
- startDistance := r.DistanceM(i)
- endDistance := r.DistanceM(i + 1)
+ startDistance := distances[i]
+ endDistance := distances[i+1]
t := (distance - startDistance) / (endDistance - startDistance)
- return r.ElevationM(i) + t*(r.ElevationM(i+1)-r.ElevationM(i))
+ return elevations[i] + t*(elevations[i+1]-elevations[i])
}
func (r *Ride) ClimbFromDist(startDist, endDist float64) Climb {
@@ -288,10 +309,21 @@ func (r *Ride) ClimbFromDist(startDist, endDist float64) Climb {
break
}
}
- return Climb{ride: *r, rideStart: start, rideEnd: end}
+ return r.climbFrom(start, end)
}
func (r *Ride) ClimbFromIndexes(startIndex, endIndex int) Climb {
kcore.Assert(startIndex >= 0 && endIndex < r.Len() && startIndex < endIndex, "invalid climb indexes")
- return Climb{ride: *r, rideStart: startIndex, rideEnd: endIndex}
+ return r.climbFrom(startIndex, endIndex)
+}
+
+func (r *Ride) climbFrom(start, end int) Climb {
+ return Climb{
+ distances: r.distances,
+ elevations: r.elevations,
+ coords: r.coords,
+ timestamps: r.timestamps,
+ rideStart: start,
+ rideEnd: end,
+ }
}
diff --git a/rideanalysis/analysis.go b/rideanalysis/analysis.go
index 90be629..55f77b8 100644
--- a/rideanalysis/analysis.go
+++ b/rideanalysis/analysis.go
@@ -39,7 +39,7 @@ func Analyze(parsed ride.Ride, passes []mountain_pass.MountainPass, officialClim
claimedClimbs := make([]AnalyzedClimb, 0)
claimedRanges := make([]ride.Range, 0)
for _, official := range officialClimbs {
- for _, matched := range official_climb.MatchClimbs(parsed.Coords, official, matchPolicy.EndpointRadiusM) {
+ for _, matched := range official_climb.MatchClimbs(parsed.Coords(), official, matchPolicy.EndpointRadiusM) {
claimedRanges = append(claimedRanges, ride.Range{Start: matched.StartIndex, End: matched.EndIndex})
claimedClimbs = append(claimedClimbs, analyzedClimb(parsed, parsed.ClimbFromIndexes(matched.StartIndex, matched.EndIndex), official.Name, official.ID))
}
@@ -72,7 +72,7 @@ func analyzedClimb(parsed ride.Ride, segment ride.Climb, name string, officialID
Name: name,
Category: ride.CategoryOf(cotacol).String(),
DistanceM: segment.EndDistanceM() - segment.StartDistanceM(),
- SlopePercent: ride.Slope(parsed, segment.StartIndex(), segment.EndIndex()) * 100,
+ SlopePercent: ride.Slope(parsed.Distances(), parsed.Elevations(), segment.StartIndex(), segment.EndIndex()) * 100,
Cotacol: cotacol,
OfficialClimbID: officialID,
OfficialName: officialClimbNameForID(name, officialID),