Skip to content
andrewdunndevPublic

About

Render Strava activity into stylized heatmap posters and inline SVG. Real polyline-circle obfuscation. Privacy flags for shippable renders.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

hero

patinate

pipeline license built with GitLab

Render Strava activity into stylized heatmap posters and inline SVG. The patina builds where you ride often: overlapping low-alpha strokes accumulate into brighter color the way actual patina builds on metal.

Grand Rapids, three themes: noir_heat, blueprint_heat, warm_beige

The gallery is fixtures/activities.json: seeded random walks over the Grand Rapids road graph. The repo ships no real-rider data.

Quickstart

cargo install patinate

(Or cargo install --git https://gitlab.com/dunn.dev/patinate to build from source.) Build needs a C compiler (build-essential / xcode-select --install). No external runtime deps.

  1. Register a Strava app at https://www.strava.com/settings/api. Authorization callback domain: localhost.

  2. Write ~/.config/patinate/config.toml:

    [general]
    city_name  = "Your City"
    country    = "USA"
    center_lat = 42.9634   # downtown
    center_lng = -85.6681
    radius_m   = 25000
    
    [render]
    theme          = "noir_heat"
    viewbox_width  = 1200
    viewbox_height = 1600
    
    [privacy]
    home_lat             = 42.9999  # YOUR home, not the map center
    home_lng             = -85.9999
    obfuscation_radius_m = 1000     # 1000m+ for anything you publish
    salt                 = "..."    # openssl rand -hex 16; never change it
    
    [strava]
    client_id     = "12345"
    client_secret = "your-strava-client-secret"
  3. patinate auth                                                     # OAuth dance
    patinate fetch-osm --city "Your City, USA" --radius-m 25000       # OSM basemap
    patinate sync                                                     # pull activities
    patinate render --theme warm_beige --cycling --out heatmap.svg    # render

patinate auth writes the file at chmod 0600 (parent dir 0700) and runs the activity:read_all OAuth scope. The dashboard's "Your Access Token" is read-only and will not work for sync.

Privacy

Nothing within obfuscation_radius_m of home reaches the SVG. The hidden zone is offset from home by a secret amount (up to offset_m, default 750 m, floor 250, seeded by [privacy].salt), edges trimmed. Tracks still converge toward home: a determined analyst can narrow it to roughly 100-250 m. A larger radius adds only modest cover. A radius needs a private, stable salt (openssl rand -hex 16).

Default-rendered SVGs embed data-rider, data-bike, data-year, data-type on heat paths plus exact center coords in <desc>. Useful for consumer-page filters; identifying in a public publication.

For public publication, also anonymize:

patinate render --theme cycle_heat --anonymize --obfuscation-radius-m 1000 --out heatmap.svg

--anonymize drops the data-* attrs and <desc>; a PNG hides no more. It also warns when [general] center sits within the radius plus offset_m of home, since the frame centers on it.

Subcommands

Command What it does
patinate auth OAuth with Strava. Tokens to ~/.config/patinate/config.toml.
patinate sync Incremental pull of activities into the SQLite cache.
patinate fetch-osm OSM basemap for a city via Overpass.
patinate render Heatmap SVG from cached activities + OSM.
patinate list-themes Show embedded themes (and any in --themes-dir).

Themes

Six themes baked into the binary. --themes-dir <dir> adds custom *.json themes alongside. The heat core multiplies on a light bg and screens on a dark one; heat.blend (normal, multiply, screen) pins it.

Preview Theme Palette Use when
noir_heat noir_heat Black background, white road hierarchy, hot orange heat. Strava-classic, default poster.
blueprint_heat blueprint_heat Deep blueprint blue, pale roads, amber heat. Engineering-doc aesthetic.
warm_beige warm_beige Cream paper, sepia roads, terracotta heat. Print output.
cycle_heat cycle_heat Paper palette + steel-blue heat. Inline web hero on a light page.
newsprint newsprint Letterpress black on gray stock, red heat. A print that reads like a page.
verdigris verdigris Bronze roads on a dark green ground, verdigris heat. A dark print for a frame.
Render flag reference
Flag Effect
--theme <name> Theme to apply. Overrides [render].theme.
--themes-dir <dir> Directory of custom *.json themes.
--cycling Filter to Ride and EBikeRide.
--type <list> Comma-separated activity types.
--gear <id> Strava gear_id filter. Repeatable.
--activity-id <n> Render a single activity by Strava ID.
--min-distance-m <m> Drop sub-m noise stubs. Default 1000.
--web Inline-embed preset: lighter glow, compact paths, no typography, minor roads or streams. It doesn't imply --anonymize; public pages pass both.
--transparent-bg Skip the background rect. For inlining over a page background.
--heat-only Skip basemap; render heat plus typography only.
--anonymize Strip data-* from heat paths and scrub <desc>. Use for public publication.
--obfuscation-radius-m <m> Override the privacy radius for one render.
--heat-bloom <f> Multiplier on the glow stack. 0 = sharp, 1 = theme default, 2+ = soft cloud.
--heat-alpha <f> Multiplier on sharp-core stroke opacity.
--radius-m <m> Override the configured map radius.
--viewbox-width <px> / --viewbox-height <px> Override viewBox dimensions.
--fit <contain|cover> contain (default) letterboxes the radius circle; cover fills the viewBox and crops. The frame never depends on rides or home.
--no-refine Skip OSM filter pipeline. Diagnostic.
--config <path> TOML config. Default ~/.config/patinate/config.toml.
--osm <path> Pre-fetched OSM JSON. Default per-user cache.
--activities <path> JSON array of activities. Default: read from cache.
--cache <path> SQLite cache path. Default per-user.
--out <path> Output SVG. Default heatmap.svg.
Embed in a page

One way to embed: render once, rasterize to WebP, ship the WebP. The consumer page provides its own typography (the --web preset skips it).

patinate render --theme cycle_heat --cycling --web --transparent-bg \
  --radius-m 12000 --viewbox-width 1200 --viewbox-height 720 --fit cover \
  --out hero.svg

rsvg-convert hero.svg -w 3600 -h 2160 -o /tmp/hero.png
magick /tmp/hero.png -quality 82 hero.webp

For inline SVG (hover, JS filters, vector zoom), skip the rasterize; it isn't a privacy step. Heat paths emit data-rider / data-bike / data-year / data-type for consumer JS. If the page is public, --anonymize first. See "Privacy" above.

Environment variables

PATINATE_OSM is read by patinate render and written by patinate fetch-osm. Pass --out explicitly if you want different paths.

Variable Purpose
STRAVA_PATINATE_CLIENT_SECRET OAuth client secret.
STRAVA_PATINATE_REFRESH_TOKEN Long-lived refresh token.
STRAVA_PATINATE_ACCESS_TOKEN Optional 24h access token.
PATINATE_CONFIG TOML config path.
PATINATE_OSM OSM JSON path (read by render, written by fetch-osm).
PATINATE_THEMES_DIR Custom themes directory.
PATINATE_OVERPASS_ENDPOINT Overpass API URL.
PATINATE_CACHE SQLite cache path.
PATINATE_ACTIVITIES Activity JSON path (skips the cache).

Generic figment override: PATINATE_<SECTION>__<KEY>. Example: PATINATE_RENDER__VIEWBOX_WIDTH=1600.

Roadmap and known limitations
  • Single-rider only. Multi-rider rendering is deferred.
  • No built-in PNG. Rasterize via rsvg-convert.
  • No JS interactivity layer. data-* attrs are emitted; the markup is intentionally inert.
  • No tiled basemap. Re-fetch OSM when you move or change radius.
  • No schema migrations. After a schema-changing bump, delete cache.db and re-sync. It sits in ~/.local/share/patinate/ (Linux), ~/Library/Application Support/dev.dunn.patinate/ (macOS) or %APPDATA%\dunn\patinate\data\ (Windows).
  • Sync is at-least-once on partial failure. The watermark only advances after a full successful sweep; idempotent upserts cover it.
  • No antimeridian handling. Cities within radius_m / (111km * cos(lat)) of ±180° produce a degenerate frame.
  • No viewport culling. OSM basemap renders in full regardless of --radius-m; rasterize for a tight file-size budget.
  • Rate limits: a Strava 429 honors Retry-After once (60s cap); Overpass 429/5xx bails. PATINATE_OVERPASS_ENDPOINT takes a mirror.
  • No JSON-log conventions. tracing keys are ad-hoc.
Architecture

Rust binary, single static target. SQLite cache via rusqlite (bundled). Figment for layered TOML and env config. geo for polyline-circle clipping. svg crate for output. oauth2 and reqwest for the Strava dance. Overpass API for OSM basemap fetch. polyline crate for decode. No JS runtime, no headless browser, no external rasterizer at render time.

Tests include a load-bearing privacy invariant (apply_privacy_invariant_loop_through_home) and an end-to-end SVG test.

Strava terms

Sync your own data. Do not redistribute Strava-derived raw activity data. The rendered SVG is yours; the underlying GPS traces are not. See https://www.strava.com/legal/api.

Security

Privacy-bypass disclosures: see SECURITY.md. Contributing: see CONTRIBUTING.md.

License

MIT. See LICENSE.


Built by Andrew G. Dunn.

About

Render Strava activity into stylized heatmap posters and inline SVG. Real polyline-circle obfuscation. Privacy flags for shippable renders.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages