diff options
| author | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-09-13 21:55:59 +0200 |
|---|---|---|
| committer | Martin Kagamino Lehoux <martin@lehoux.net> | 2026-09-13 21:55:59 +0200 |
| commit | e3d0beee5be3dd2854b0f4fc10905a4fcdc08f47 (patch) | |
| tree | ec8234d3442c498ec170b6444bd2cfae5b29a476 /web/official-climbs.go | |
| parent | db4d2b40895db41ce188549f49a6f277ba95c890 (diff) | |
Persist official climb attempts with climbs pages
Record one attempt row per (ride, climb, occurrence) when a ride detail
loads, snapshotting distance, elevation gain, slope, cotacol and attempt
timestamps; a climbs-backfill CLI command seeds all existing rides.
New /climbs list page aggregates attempts (medians computed in Go, best
time as min duration) with sortable columns, and /climbs/{id} shows the
median scalar stats, a median normalized elevation profile (50 steps
over each attempt's own length), the full-precision recorded route of
the median-distance attempt on an OSM map, and every attempt with time
at second precision.
Diffstat (limited to 'web/official-climbs.go')
| -rw-r--r-- | web/official-climbs.go | 60 |
1 files changed, 60 insertions, 0 deletions
diff --git a/web/official-climbs.go b/web/official-climbs.go new file mode 100644 index 0000000..ce9d7cc --- /dev/null +++ b/web/official-climbs.go @@ -0,0 +1,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) + } +} |
