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
|
package rideanalysis
import (
"math"
"github.com/jftuga/geodist"
"github.com/martinlehoux/biking_home/climb"
"github.com/martinlehoux/biking_home/official_climb"
"github.com/martinlehoux/biking_home/ride"
)
// ClimbProfile is the chart profile of one official climb, merged from the rides that recorded it.
type ClimbProfile struct {
DistanceM float64
Route []geodist.Coord
Sections []climb.ProfileSection
}
// MergeClimbProfile merges every recorded passage of an official climb into one profile: the median
// elevation at every step of an axis as long as the climb's aggregate, the recorded route of the
// median-length passage (full precision, no resampling), and that trace cut into display sections.
func MergeClimbProfile(officialClimb official_climb.OfficialClimb, rides []ride.Ride, aggregate official_climb.Aggregate, matchPolicy official_climb.MatchPolicy, profilePolicy climb.ProfilePolicy) ClimbProfile {
profile := ClimbProfile{DistanceM: aggregate.DistanceM}
elevationSamples := make([][]float64, 0, len(rides))
passages := make([]climb.Climb, 0, len(rides))
for _, parsed := range rides {
for _, matched := range official_climb.MatchClimbs(parsed.Route, officialClimb, matchPolicy.EndpointRadiusM) {
passage := climb.Climb{Route: parsed.Route.Slice(matched.StartIndex, matched.EndIndex)}
if sample := official_climb.NormalizeProfile(passage); len(sample) > 0 {
elevationSamples = append(elevationSamples, sample)
}
passages = append(passages, passage)
}
}
profile.Route = medianPassageRoute(passages)
profile.Sections = profileRoute(aggregate.DistanceM, official_climb.MedianProfile(elevationSamples)).ProfileSections(profilePolicy)
return profile
}
// medianPassageRoute picks the recorded route of the passage whose length is the median: robust to
// one drifted ride.
func medianPassageRoute(passages []climb.Climb) []geodist.Coord {
if len(passages) == 0 {
return nil
}
distances := make([]float64, len(passages))
for index, passage := range passages {
distances[index] = passage.Distances[len(passage.Distances)-1] - passage.Distances[0]
}
median := official_climb.Median(distances)
best := passages[0]
bestDiff := math.Abs(distances[0] - median)
for index := 1; index < len(passages); index++ {
if diff := math.Abs(distances[index] - median); diff < bestDiff {
best = passages[index]
bestDiff = diff
}
}
return best.Coords
}
// profileRoute lays the merged elevation samples on a uniform axis as long as the climb.
func profileRoute(distanceM float64, elevations []float64) climb.Route {
if len(elevations) < 2 {
return climb.Route{}
}
route := climb.Route{Distances: make([]float64, len(elevations)), Elevations: elevations}
for index := range elevations {
route.Distances[index] = distanceM * float64(index) / float64(len(elevations)-1)
}
return route
}
|