A web application to visualize your Garmin Connect activities on an OpenStreetMap map with detailed GPS tracks and category breakdowns.
- Full GPS Track Visualization: Displays complete activity routes on the map with Canvas rendering for optimal performance
- Multi-Year Activity Data: Shows all activities from the last 10 years with year-based filtering
- Smart Categorization:
- Cross-country skiing (classic, skating, double poling)
- Roller skiing (classic, skating, double poling)
- Running (including trail and track running)
- Automatic detection based on activity types and names
- Race Detection: Activities marked as races in Garmin Connect display trophy icons 🏆
- Color-Coded Display:
- Running: Red variants (#FF4136 for running, #DC143C for trail, #FF6B6B for track)
- Winter cross-country skiing: Blue (#0074D9)
- Roller skiing: Green (#2ECC40)
- Other activities: Grey gradients with reduced opacity
- Category Statistics: Grouped distance totals with subcategory breakdowns
- Two-Tier Caching:
- Raw API data cached separately for faster reprocessing
- Processed activities cached for 6 hours to reduce API calls
- Year Filtering: Toggle visibility of activities by year (all 10 years shown by default)
- Black & White Map: Clean CartoDB Positron basemap for better activity visibility
The application automatically categorizes activities based on Garmin activity types and naming conventions:
Activities are classified as cross-country skiing if the activity type includes:
- "cross_country_skiing"
- "cross" or "skiing"
- "multi_sport"
Technique Detection (applied to both winter and roller skiing):
- Double Poling: Activity name contains "dp" or "stak"
- Skating: Activity name or type contains "skat"
- Classic: Activity name or type contains "classic", or used as default if no other technique detected
Winter vs Roller Skiing:
- Roller Skiing: Activity name contains "roller" → categorized as
roller_[technique] - Winter XC Skiing: All other XC activities → categorized as
xc_[technique]
- Track Running: Activity type is "track_running"
- Trail Running: Activity type is "trail_running"
- Running: All other running activity types
Activities are marked as races (🏆) when eventType.typeKey == "race" in the Garmin API response.
All other activity types (cycling, walking, etc.) are displayed with grey gradients and reduced opacity.
- Python 3.11 or higher
- uv package manager
- Garmin Connect account
- Active internet connection
-
Clone or navigate to the project directory:
cd /path/to/connectmap -
Install dependencies:
uv sync
This will create a virtual environment and install all required packages.
-
Copy the environment template:
cp .env.example .env
-
Generate OAuth session token:
uv run setup_oauth.py
Follow the prompts to enter your Garmin Connect email and password. This is a one-time setup.
-
Copy the generated token:
The script will output a
GARMIN_SESSIONtoken. Copy this value and add it to your.envfile:GARMIN_SESSION=your_session_token_hereNote: The session token is required. The application will not start without it.
-
Start the Flask server:
uv run app.py
You'll see output showing the progress of loading activities:
Fetching activities from 2016-01-01 to 2025-12-31... Found 150 activities for 2025 Downloading raw data for 2025 activities... Downloading 2025: 100%|████████| 150/150 [03:30<00:00, 1.4s/activity] Processing 2025 activities... Processing 2025: 100%|████████| 150/150 [00:15<00:00, 9.7activity/s] Cached raw data for 150 activities in 2025 Cached 148 activities for 2025Note: The first load may take several minutes per year as it downloads GPS data for all activities. Subsequent loads will use cached data.
-
Open your browser:
Navigate to http://localhost:5000. You'll see a map showing all activities from the last 10 years with GPS tracks color-coded by type. Use the year checkboxes to filter which years are displayed.
Activities are cached locally in the cache/ directory with two cache types:
- Raw cache (
raw_cache_{year}.json): Original API responses from Garmin - Processed cache (
activities_cache_{year}.json): Processed activity data for the map
Both caches expire after 6 hours. To force a refresh:
# Clear all caches (forces re-download from Garmin API)
rm -rf cache/
# Clear only processed caches (reprocesses from raw cache without API calls)
rm cache/activities_cache_*.json
# Clear single year cache
rm cache/activities_cache_2025.json cache/raw_cache_2025.jsonThen restart the application.
The project includes helper scripts for common development tasks:
# Lint all code
./bin/lint.sh
# Format all code (Python with ruff, Markdown with prettier)
./bin/format.shPre-commit hooks are configured to automatically lint and format code before commits:
# Install pre-commit hooks (already done during setup)
uv run pre-commit install
# Run pre-commit on all files manually
uv run pre-commit run --all-filesHooks include:
- Ruff linting and formatting (Python)
- Prettier formatting (Markdown)
- Trailing whitespace removal
- End-of-file fixes
- YAML/TOML validation
- Private key detection
The project uses:
- Ruff for Python linting and formatting
- Prettier for Markdown formatting
Both are configured to run automatically on save in VS Code and via pre-commit hooks.
Manual formatting:
uv run ruff check --fix .
uv run ruff format .
npx prettier --write "*.md"- Backend: Flask (Python)
- Frontend: HTML, JavaScript, Leaflet.js
- Maps: OpenStreetMap (CartoDB Positron)
- Data Source: Garmin Connect API (OAuth)
- Package Manager: uv