aboutsummaryrefslogtreecommitdiff
path: root/README.md
blob: 34ec5733c415cbc47d18ae8cbc3e44a16fcfcb95 (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
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217

# 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

**Data import** - Sync rides from Strava, geo, time, heart rate, power, cadence.
Rate limit aware and visible progress. 

**Climb detection** - Auto detect climbs with difficulty. Save official climbs for rematching.
Adaptive climb profile: drawn steps follow the slope changes.

**Mountain pass** - Match climbs with mountain pass from OSM, and display on map.

**Rides** - Display rides forever, with filtering and sorting. Categorise all rides.

**Analytics** - Plot speed vs difficulty, difficulty vs duration. Color points by type or season.

**Preview** - Upload a raw GPX or FIT file (Activity or Course) at `/preview` to analyze it (climb detection, official-climb matching, mountain-pass crossings) without importing it; official climbs can be saved from a preview.

**Elevation fix** - Hover the profile to see `#index · km · m`, run `go run . elevation analyse <id|all>` to flag unrealistic grades (>50% over <30m) and `go run . elevation fix <id> --from <index|km> --to <index|km> [--dry-run]` (or `--auto`) to linearly interpolate elevations between anchors, rewriting FIT+GPX (`.bak` kept) and recomputing Cotacol.

## Getting started

```bash
mise run build
mise run dev-watch # Start the web server on http://localhost:8080
go run . fetch-osm # Download the France OSM PBF (resumable)
go run . extract-osm france-latest.osm.pbf # Extract mountain passes from a France OSM PBF
go run . backfill # Recompute ride values and official climb attempts for every stored ride
go run . backtest # Benchmark computed vs stored ride values
go run . -- -pprof # Expose pprof endpoints at `/debug/pprof`
docker compose up -d pyroscope # Start the local Pyroscope server on http://localhost:4040
```

Continuous profiling with Pyroscope: the app pushes CPU, memory, and goroutine
profiles every 10s when `pyroscope.enabled` is set in `config.yaml`. Run
`docker compose up -d pyroscope` to start the ingest server, then browse
`http://localhost:4040` to inspect the `biking_home` application profile.


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.

## OSM

Passes are French OSM PBF nodes with `mountain_pass=yes` (or legacy `natural=mountain_pass`), stored with name, elevation and coordinates.

An OSM way is an ordered list of nodes; its `highway` tag describes the road type. For each pass we collect the ways containing its node ID and map `highway` road values (`primary`/`secondary`/…/`service`) → `road`, `track` → `track`, `path`/`footway` → `path` (plus `natural=rock`/`sport=climbing` → `hiking`, no way → `unknown`), coloured distinctly on the ride detail map.

## Architecture

- Go 1.27; SQLite via `mattn/go-sqlite3`
- `geo` — coordinate distance, nearest-point, and shared `Interval` primitives; dependency-free
- `climb` — climb detection and scoring. A `Route` is the geometry-only trace (distances, elevations, coords), and `Detect` returns `[]DetectedClimb` (a climb plus its ride-relative start index). Provides slope and Cotacol difficulty scoring, and `ProfileSections` cuts a trace into the chart steps (widest bends first, bounded by a `ProfilePolicy`). Deliberately decoupled from `ride`
- `ride` — GPX parsing and the ride value (embeds `climb.Route` for geometry, adding timestamps and telemetry), moving-time metrics, and SQLite persistence for imported ride metadata and versioned computed values
- `official_climb` — matches official climbs against a route by endpoint coordinates and the recorded sample of the segment the climb was created from: a candidate passage outside ±50% of that sample's length or elevation gain is not the climb
- `mountain_pass` — OSM PBF extraction (`mountain_pass=yes` / `natural=mountain_pass` nodes), pass storage and querying, and crossings/climb matching on `climb.Climb`
- `rideanalysis` — orchestrates a ride into analyzed climbs and mountain-pass crossings, snapshots its official-climb attempts, and merges the recorded passages of an official climb into one chart profile
- `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
- `backfill` — recomputes the data derived from stored rides: ride values first, then official climb attempts. The migration tool behind `go run . backfill`
- `cli` — command entry points: `main.go` dispatches, and command bodies live with the packages they drive (`ride` import/benchmarks, `backfill`, `mountain_pass` OSM)
- test builders `internal/routetest`, `internal/ridetest`, `internal/dbtest`

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

    main["main (bootstrap: main.go)"]
    cli["cli (import, backfill, benchmark)"]
    backfill["backfill (ride values, climb attempts)"]
    web["web (HTTP + templ, sync)"]
    rideanalysis["rideanalysis (climbs + crossings)"]
    ride["ride (GPX parsing, Ride value, metrics, persistence)"]
    climb["climb (Route, detection, Cotacol)"]
    official_climb["official_climb (official climb matching)"]
    mpass["mountain_pass (OSM PBF, crossings, matching)"]
    geo["geo (distance, nearest, Interval)"]
    strava["strava (OAuth, List, Get)"]
    config["config (YAML)"]

    main --> cli
    main --> config
    cli --> web
    cli --> ride
    cli --> mpass
    cli --> config
    cli --> backfill

    backfill --> ride
    backfill --> rideanalysis
    backfill --> official_climb
    backfill --> mpass
    backfill --> config

    web --> rideanalysis
    web --> strava
    web --> ride
    web --> mpass
    web --> official_climb
    web --> config
    web --> climb

    rideanalysis --> ride
    rideanalysis --> climb
    rideanalysis --> mpass
    rideanalysis --> official_climb

    ride --> climb

    mpass --> climb
    mpass --> geo

    official_climb --> geo

    config --> official_climb
```

## 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: go run . serve --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
```

## TODO

- Do not use json tag in output, use json builders?
- http://localhost:8080/rides/436: remove train part?
- also http://localhost:8080/rides/438
- Garmin GPX: elevation, time, temp, HR, cadence
- Garmin TCX: stats (cal, HR, time, cadence) + pos, alt, distance, HR, cadence, speed
- Garmin FIT: time, sport, lap/split, gps, sensor, events
- 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: analyze ride?
- Road quality (Arbois = 2/5, Roquefavour = 4/5)
- display avg power for climb (ride, climb)
- les 100 cols

### Official Climb

- Keep trace apart from ride to display slope profile, compute cotacol
- Record ride times on official climbs
- Destination via Start

### Multi activity (Raid, Tour)

### Blog with pictures and markdown

### Ride planning

https://gemini.google.com/app/1428ae2e5743e4ff

## 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