# 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. Climb profile by 100m step. **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. ## 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 and persist all ride computed values 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. ## 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. 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 - `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 - `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 - 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)"] 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 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 - 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 - 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