aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 1ae3a8befff7f0ba92ee6587dd52c0162c43c73c (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
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186

# biking_home — cycling ride analysis

A Go toolkit for analyzing cycling rides from GPX exports: parse rides, detect climbs, score their difficulty, match the same climb across rides, and load French mountain passes from OpenStreetMap into SQLite.

## Features

- **GPX parsing** — reads elevation-tracked rides from Garmin/Strava GPX exports into a `Ride` model
- **Climb detection** — automatically finds climbs within a ride, each scored and categorized (Cat 4 … HC), requiring at least a 3.5% average slope (gentler stretches are re-segmented into their qualifying parts)
- **Difficulty score (Cotacol)** — scores any ride or climb by splitting it into 100 m segments and summing `0.1 km × slope²` per segment. This fixed step is a deliberate compromise: smaller steps increase sensitivity to elevation noise, while unconstrained variable steps maximize noise rather than meaningful difficulty.
- **Similar-climb matching** — finds the same climb across rides by matching start/end coordinates, so times can be compared
- **Mountain pass extraction** — extracts French mountain passes from an OpenStreetMap PBF export (nodes tagged `mountain_pass=yes` / `natural=mountain_pass`) into a SQLite database
- **Pass crossing detection** — flags which passes a ride crosses and names each climb after the pass it tops (e.g. "Col de Castellaras")
- **Interactive ride profiles** — explores elevation, climbs, and mountain-pass crossings in the web ride detail view
- **Official climb cards** — separates matched climbs from pending matches and expands each matched climb with its own elevation profile
- **Repeated official climbs** — matches the same official climb once per lap on looping rides, respecting direction
- **Web ride library** — starts a local web server by default, imports Strava rides over a date range, stores metadata in SQLite, and keeps their GPX files on disk
- **Materialized ride values** — computes Cotacol on import, stores its algorithm version in SQLite, and refreshes all computed values with `-backfill`
- **Strava stream metrics** — preserves heart rate, cadence, and power in Garmin-compatible GPX files and in-memory ride columns
- **Metadata-only ride imports** — stores Strava activity statistics for rides without GPS tracks, without Cotacol or route data
- **Ride table sorting** — sorts every ride-library column through clickable server-side headers and query parameters
- **Ride library filters** — filters rides by distance, moving duration, start date, Cotacol score, and route availability through shareable query URLs
- **Computed moving time** — computes moving time from the GPX track with a speed-threshold algorithm, persists it as the canonical value (keeping Strava's as `external_moving_time_s`), and benchmarks against that reference via `-backtest`
- **CLI progress bars** — shows a live progress bar for the long-running CLI processes (`-fetch-osm`, `-extract-osm`, `-backfill`, `-backtest`)
- **Verified OSM download** — verifies the France PBF against geofabrik's published MD5 after downloading (and on already-complete files), keeping a corrupt file for inspection instead of silently trusting its size
- **Ride detail maps** — opens a stored ride with its recorded route on an interactive OpenStreetMap map
- **Strava import progress** — streams live import progress with an aggregate progress bar and prevents overlapping syncs
- **Analytics second chart** — plots normalized difficulty (Cotacol per 100 km) against moving duration alongside speed vs. Cotacol
- **Ride categorisation** — mandatory bike type (road/mtb) and ride type (training/indoor/race/transport) per ride, bulk-categorised via a data-quality todo, filtered on library and analytics, and benchmarked excluding indoor/transport
- **Ride type multiselect filter** — pick multiple ride types at once via a checkbox group on the library and analytics, defaulting to all except transport

## Getting started

```bash
mise run build
```

## Usage

```bash
# Start the web server on http://localhost:8080
mise run dev-watch
```

## Options / Configuration

| Flag | Description | Default |
| --- | --- | --- |
| `-fetch-osm` | Download the France OSM PBF (resumable) | `false` |
| `-extract-osm` | Extract mountain passes from a France OSM PBF | `""` |
| `-backfill` | Recompute and persist all ride computed values | `false` |
| `-backtest` | Benchmark computed vs stored ride values | `false` |
| `-pprof` | Expose pprof endpoints at `/debug/pprof` | `false` |

Configuration is stored in `config.yaml`. Start from `config.example.yaml` and set the Strava credentials before launching the server.

## Strava API

The Strava v3 API (OAuth2) is the platform's public access point; scopes gate each resource (`read`, `activity:read_all`, `activity:write`, `push:subscriptions`). It offers:

- **Activities** — list and fetch ride detail, plus data streams (lat/lng, altitude, time, distance, heart rate, cadence, power) to rebuild GPX or compute metrics
- **Segments** — explore segments, match segment efforts, pull personal records
- **Uploads** — create activities by pushing FIT, GPX or TCX files
- **Webhooks** — push subscriptions notify a callback URL on activity create/delete, enabling real-time sync
- **Extras** — clubs, routes, gear, photos, kudos

Standard apps are rate-limited to 100 calls per 15 minutes and 1,000 per day.

## Architecture

- Go 1.27; SQLite via `mattn/go-sqlite3`
- `ride` — GPX parsing, climb detection, difficulty scores (KOM + Cotacol), and SQLite persistence for imported ride metadata and versioned computed values
- `mountain_pass` — OSM PBF extraction (`mountain_pass=yes` / `natural=mountain_pass` nodes), pass storage and querying, and crossings/climb matching
- `strava` — OAuth2 client returning activity metadata and GPX data
- `config` — typed YAML configuration and atomic persistence
- `web` — HTTP server, OAuth callback, sync orchestration, and templ pages
- `cli` — command handlers for data import, backfill, and benchmarking

```mermaid
flowchart TB
    %% Arrow X --> Y means: X depends on Y

    main["main (bootstrap: main.go)"]

    ride["ride (GPX parsing, climbs, Cotacol, persistence)"]
    mpass["mountain_pass (OSM PBF, crossings)"]
    strava["strava (OAuth, List, Get)"]
    config["config (YAML)"]
    web["web (HTTP + templ)"]
    cli["cli (import, backfill, benchmark)"]

    main --> cli
    cli --> ride
    cli --> mpass
    cli --> web
    mpass --> ride
    web --> strava
    web --> ride
    web --> config
```

## Research

MapLibre can render local vector-tile maps in the browser from OpenStreetMap PBF data after preprocessing. It supports GeoJSON overlays, dynamic styling, interactive points and lines, feature selection, and offline use with locally served tiles, styles, fonts, and sprites. This enables rich map layers without depending on external basemap tile services.

## Development

```bash
mise run check
mise run build
```

TypeScript checks can also be run directly with `npm run check`, covering formatting, linting, compilation, type checking, and tests.

## Profiling

Answer "what slows the server" with function-level CPU profiles grouped by route.

```bash
# Start the server with pprof (exposes http://localhost:8080/debug/pprof/)
go run . -pprof
# or: mise run dev -- -pprof
```

```bash
# Capture 10s CPU sample and open interactive UI at http://localhost:8081
go tool pprof -http=:8081 http://localhost:8080/debug/pprof/profile?seconds=10
```

While it samples (10s), hit the routes you care about:

```bash
for _ in {1..50}; do curl -s http://localhost:8080/rides/1 >/dev/null; curl -s http://localhost:8080/ >/dev/null; done
```

In the UI: `VIEW > Flame Graph`, `Top`, `Source` (annotates `ride/ride.go:90` etc.). Each request is tagged with `route` via `runtime/pprof` labels (`GET /`, `GET /rides/{id}`, `GET /analytics`, …), so filter by route:

```bash
go tool pprof -http=:8081 -tagfocus='route:GET /rides/{id}' http://localhost:8080/debug/pprof/profile?seconds=10
```

Other endpoints:

```bash
go tool pprof -http=:8081 http://localhost:8080/debug/pprof/heap   # allocs
```

Optional continuous profiling with Pyroscope (local docker, ~1-2% overhead):

```bash
docker run --rm -p 4040:4040 grafana/pyroscope
```

Add `github.com/grafana/pyroscope-go` and call `pyroscope.Start` with `ServerAddress: "http://localhost:4040"` and `route` label — gives the same flamegraphs but over time. Keep it only if pprof snapshots are not enough.

## TODO

- pyroscope
- screaming archi + hexagon
- Compute estimated power, speed
- For a ride, scatter, one point per 100m segment. Slope vs speed
- Plot speed and slope per segment, colored by heart rate
- http://localhost:8080/rides/378: Repentance partial climb
- Auto detect new climbs from raw map
- Perf
    - improve GPX parsing and Analyze
    - Compact binary or vectorized storage for root
    - Actually might be .fit
- Strava
    - Resync metadata (title) from list
- OffClimb
    - Keep trace appart from ride to display slope profile, compute cotacol
    - Profile - Adaptative to show ramps
    - Record ride times on official climbs
    - Destination via Start
- Mountain pass
    - issue: many passes are pedestrian (cf Marseille Calanques, http://localhost:8080/rides/397)
- Ride planning
- Road quality (Arbois = 2/5, Roquefavour = 4/5)
- Blog with pictures and markdown

## Resources

- [Climbfinder: difficulty points](https://climbfinder.com/en/difficulty-points) — the Cotacol formula behind the difficulty score
- [Pogacar 21 July 2022](https://www.strava.com/activities/7505784085) — example ride used for climb detection
- [gpxgo](https://github.com/tkrajina/gpxgo) — GPX parsing library
- [geodist](https://github.com/jftuga/geodist) — haversine distance used for climb matching