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 }