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
|
package web
import (
"log/slog"
"github.com/martinlehoux/biking_home/mountain_pass"
"github.com/martinlehoux/biking_home/official_climb"
"github.com/martinlehoux/biking_home/ride"
"github.com/martinlehoux/biking_home/rideanalysis"
)
// analysisFor runs the ride analysis pipeline the same way the ride profile
// does, so recorded attempts and displayed climbs always agree.
func analysisFor(parsed ride.Ride, passes []mountain_pass.MountainPass, officialClimbs []official_climb.OfficialClimb, matchPolicy official_climb.MatchPolicy) rideanalysis.Result {
return rideanalysis.Analyze(parsed, passes, officialClimbs, matchPolicy)
}
// attemptsFromAnalysis extracts one attempt per official-climb occurrence.
// Occurrences are numbered per climb in route order (0, 1, … for laps).
func attemptsFromAnalysis(rideID int64, analysis rideanalysis.Result, parsed ride.Ride) []official_climb.Attempt {
occurrences := make(map[int64]int)
attempts := make([]official_climb.Attempt, 0)
for _, analyzed := range analysis.Climbs {
if analyzed.OfficialClimbID == 0 {
continue
}
occurrence := occurrences[analyzed.OfficialClimbID]
occurrences[analyzed.OfficialClimbID] = occurrence + 1
attempts = append(attempts, official_climb.AttemptFromStats(rideID, occurrence, official_climb.AttemptStats{
OfficialClimbID: analyzed.OfficialClimbID,
DistanceM: analyzed.DistanceM,
ElevationGainM: elevationGain(analyzed),
SlopePercent: analyzed.SlopePercent,
Cotacol: analyzed.Cotacol,
StartIndex: analyzed.Interval.Start,
EndIndex: analyzed.Interval.End,
}, parsed.Timestamps()))
}
return attempts
}
func elevationGain(analyzed rideanalysis.AnalyzedClimb) float64 {
gain := 0.0
for i := 1; i < len(analyzed.Segment.Elevations); i++ {
if diff := analyzed.Segment.Elevations[i] - analyzed.Segment.Elevations[i-1]; diff > 0 {
gain += diff
}
}
return gain
}
// recordOfficialClimbAttempts persists the ride's official-climb attempts,
// replacing any previously recorded ones. Failures are logged, never fatal:
// attempts are an optimization over on-the-fly matching.
func (s *Server) recordOfficialClimbAttempts(rideID int64, parsed ride.Ride, analysis rideanalysis.Result) {
attempts := attemptsFromAnalysis(rideID, analysis, parsed)
if err := official_climb.ReplaceRideAttempts(s.db, rideID, attempts); err != nil {
slog.Warn("Failed to record official climb attempts", "ride_id", rideID, "error", err)
}
}
|