aboutsummaryrefslogtreecommitdiff
path: root/rideanalysis/analysis.go
blob: 81c9a98d84032a3f12291096d8b79b567a7399e7 (plain) (blame)
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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

package rideanalysis

import (
	"slices"

	"github.com/martinlehoux/biking_home/climb"
	"github.com/martinlehoux/biking_home/geo"
	"github.com/martinlehoux/biking_home/mountain_pass"
	"github.com/martinlehoux/biking_home/official_climb"
	"github.com/martinlehoux/biking_home/ride"
)

const (
	passMatchRadiusM                = 300
	passMatchElevationToleranceM    = 50
	passCrossingRadiusM             = 100
	passCrossingElevationToleranceM = 25
)

// PassMarginM is how far around a ride's track mountain passes are loaded for matching and display.
const PassMarginM = 10_000

type Result struct {
	Climbs    []AnalyzedClimb
	Crossings []mountain_pass.Crossing
}

type AnalyzedClimb struct {
	Segment         climb.Climb
	Name            string
	Category        string
	DistanceM       float64
	ElevationGainM  float64
	SlopePercent    float64
	Cotacol         float64
	OfficialClimbID int64
	OfficialName    string
	Interval        geo.Interval
}

func Analyze(parsed ride.Ride, passes []mountain_pass.MountainPass, officialClimbs []official_climb.OfficialClimb, matchPolicy official_climb.MatchPolicy) Result {
	result := Result{
		Climbs:    make([]AnalyzedClimb, 0),
		Crossings: make([]mountain_pass.Crossing, 0),
	}
	claimedClimbs := make([]AnalyzedClimb, 0)
	claimedRanges := make([]geo.Interval, 0)
	for _, official := range officialClimbs {
		for _, matched := range official_climb.MatchClimbs(parsed.Route, official, matchPolicy.EndpointRadiusM) {
			interval := geo.NewInterval(matched.StartIndex, matched.EndIndex)
			claimedRanges = append(claimedRanges, interval)
			claimedClimbs = append(claimedClimbs, analyzedClimb(routeClimb(parsed.Route, interval), interval, official.Name, official.ID))
		}
	}
	slices.SortFunc(claimedRanges, func(a, b geo.Interval) int { return a.Start - b.Start })
	for _, detected := range climb.Detect(parsed.Route, claimedRanges, climb.ClimbSlopeMinimum) {
		name := ""
		if matchedPass, ok := mountain_pass.MatchClimb(detected.Climb, passes, passMatchRadiusM, passMatchElevationToleranceM); ok {
			name = matchedPass.Name
		}
		interval := geo.NewInterval(detected.Start, detected.Start+len(detected.Climb.Distances)-1)
		result.Climbs = append(result.Climbs, analyzedClimb(detected.Climb, interval, name, 0))
	}
	climbs := make([]AnalyzedClimb, 0, len(claimedClimbs)+len(result.Climbs))
	climbs = append(climbs, claimedClimbs...)
	climbs = append(climbs, result.Climbs...)
	slices.SortFunc(climbs, func(a, b AnalyzedClimb) int {
		return a.Interval.Start - b.Interval.Start
	})
	result.Climbs = climbs
	for _, crossing := range mountain_pass.DetectCrossings(parsed.Coords, parsed.Distances, parsed.Elevations, passes, passCrossingRadiusM, passCrossingElevationToleranceM) {
		result.Crossings = append(result.Crossings, crossing)
	}
	return result
}

// routeClimb returns the climb segment of route covered by interval.
func routeClimb(route climb.Route, interval geo.Interval) climb.Climb {
	return climb.Climb{
		Distances:  route.Distances[interval.Start : interval.End+1],
		Elevations: route.Elevations[interval.Start : interval.End+1],
		Coords:     route.Coords[interval.Start : interval.End+1],
	}
}

func analyzedClimb(segment climb.Climb, interval geo.Interval, name string, officialID int64) AnalyzedClimb {
	cotacol := segment.DifficultyScore()
	sample := segment.Sample(0, len(segment.Distances)-1)
	return AnalyzedClimb{
		Segment:         segment,
		Name:            name,
		Category:        climb.CategoryOf(cotacol).String(),
		DistanceM:       sample.DistanceM,
		ElevationGainM:  sample.ElevationGainM,
		SlopePercent:    climb.Slope(segment.Distances, segment.Elevations) * 100,
		Cotacol:         cotacol,
		OfficialClimbID: officialID,
		OfficialName:    officialClimbNameForID(name, officialID),
		Interval:        interval,
	}
}

func officialClimbNameForID(name string, officialID int64) string {
	if officialID == 0 {
		return ""
	}
	return name
}