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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
// Package backfill recomputes the data derived from stored rides: ride values and official climb attempts.
package backfill
import (
"database/sql"
"fmt"
"log/slog"
"slices"
"github.com/martinlehoux/biking_home/config"
"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"
"github.com/martinlehoux/kagamigo/kcore"
"github.com/schollz/progressbar/v3"
)
// Run recomputes ride values, then official climb attempts.
func Run(db *sql.DB, configPath string) {
ride.BackfillCmd(db)
var bar *progressbar.ProgressBar
count, err := ClimbAttempts(db, configPath, func(current, total int) {
if bar == nil && mountain_pass.ProgressBarEnabled() {
bar = progressbar.Default(int64(total), "Backfilling climb attempts")
}
if bar != nil {
bar.Set(current)
}
})
kcore.Expect(err, "failed to backfill climb attempts")
slog.Info("Backfilled climb attempts", "rides", count)
}
// ClimbAttempts re-records the official climb attempts of every route-bearing ride and flags the climbs the sample band emptied.
func ClimbAttempts(db *sql.DB, configPath string, reporters ...func(current, total int)) (int, error) {
appConfig, err := config.Load(configPath)
if err != nil {
return 0, err
}
matchPolicy := official_climb.DefaultMatchPolicy()
matchPolicy.EndpointRadiusM = appConfig.OfficialClimb.MatchRadiusM
before, err := climbAttemptCounts(db)
if err != nil {
return 0, err
}
items, err := ride.List(db)
if err != nil {
return 0, err
}
officialClimbs, err := official_climb.List(db)
if err != nil {
return 0, err
}
report := func(int, int) {}
if len(reporters) > 0 {
report = reporters[0]
}
backfilled := 0
for i, item := range items {
report(i+1, len(items))
if item.FitPath == "" && item.GPXPath == "" {
continue
}
parsed, err := ride.ParseStoredRide(item)
if err != nil {
slog.Warn("Failed to parse ride for climb backfill", "ride_id", item.ID, "error", err)
continue
}
passes, err := mountain_pass.LoadMountainPassesAroundRide(db, parsed.Coords, rideanalysis.PassMarginM)
if err != nil {
slog.Warn("Failed to load passes for climb backfill", "ride_id", item.ID, "error", err)
}
analysis := rideanalysis.Analyze(parsed, passes, officialClimbs, matchPolicy)
attempts := rideanalysis.Attempts(item.ID, analysis, parsed)
if err := official_climb.ReplaceRideAttempts(db, item.ID, attempts); err != nil {
slog.Warn("Failed to record climb attempts", "ride_id", item.ID, "error", err)
continue
}
backfilled++
}
if err := reportAttemptDrops(db, before); err != nil {
return backfilled, err
}
return backfilled, nil
}
// climbAttemptCounts returns the number of recorded attempts per climb.
func climbAttemptCounts(db *sql.DB) (map[int64]int, error) {
rows, err := db.Query("SELECT climb_id, COUNT(*) FROM ride_official_climb_attempts GROUP BY climb_id")
if err != nil {
return nil, fmt.Errorf("count climb attempts: %w", err)
}
defer rows.Close()
counts := make(map[int64]int)
for rows.Next() {
var (
climbID int64
count int
)
if err := rows.Scan(&climbID, &count); err != nil {
return nil, fmt.Errorf("scan climb attempt count: %w", err)
}
counts[climbID] = count
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("iterate climb attempt counts: %w", err)
}
return counts, nil
}
// reportAttemptDrops flags the climbs that lost attempts to the sample band; one keeping less than half needs review.
func reportAttemptDrops(db *sql.DB, before map[int64]int) error {
after, err := climbAttemptCounts(db)
if err != nil {
return err
}
climbIDs := make([]int64, 0, len(before))
for climbID := range before {
climbIDs = append(climbIDs, climbID)
}
slices.Sort(climbIDs)
for _, climbID := range climbIDs {
previous, kept := before[climbID], after[climbID]
if kept >= previous {
continue
}
if kept*2 < previous {
slog.Warn("Climb kept less than half of its attempts; review its sample", "climb_id", climbID, "before", previous, "after", kept)
continue
}
slog.Info("Climb attempts dropped outside the sample band", "climb_id", climbID, "before", previous, "after", kept)
}
return nil
}
|