Explore fitness activity data spatially in QGIS.
qfit is a QGIS plugin that turns synced fitness activities into GeoPackage-backed layers, analysis views, and atlas-ready publish data for mapping and print workflows.
This README is split into two parts:
- Part 1, for human readers using qfit in QGIS
- Part 2, for contributors and AI coding agents changing the codebase
qfit currently supports:
- connecting to Strava with
client_id,client_secret, andrefresh_token - opening the Strava authorize page from the plugin and exchanging an auth code for a refresh token
- fetching activities from Strava in the background
- previewing fetched activities in the dock before writing anything to disk
- storing a canonical local GeoPackage sync store
- loading QGIS layers for tracks, start points, and optional sampled stream points
- filtering by activity type, text search, date range, distance, and detailed-stream availability
- applying visualization presets, optional temporal wiring, and an optional Mapbox basemap
- running analysis workflows such as frequent starting points and activity heatmaps
- generating atlas-ready publish layers and exporting a PDF atlas from the dock
qfit uses a GeoPackage as both local sync store and QGIS data source.
Internal tables
activity_registrysync_state
Visible layers
activity_tracksactivity_startsactivity_points(optional, derived from detailed streams)activity_atlas_pages
Atlas helper tables
atlas_document_summaryatlas_cover_highlightsatlas_page_detail_itemsatlas_toc_entriesatlas_profile_samples
- Configure Strava credentials.
- Fetch activities and preview the result in the dock.
- Choose an output
.gpkgand store the data. - Load the qfit layers into QGIS.
- Apply visualization or analysis workflows.
- Optionally generate atlas-ready publish data and export a PDF atlas.
You need:
client_idclient_secretrefresh_token
qfit includes a built-in OAuth helper for the refresh-token step.
See:
docs/strava-setup.md
qfit can load an optional Mapbox basemap and keep it below the qfit layers in the QGIS layer tree.
The current visualization flow supports:
- semantic activity styling by activity type
- simpler line-based presets
- track points / start points / heatmap-oriented views
- temporal timestamp wiring when timestamp fields are available
See:
docs/map-style-guide.md
qfit can generate atlas-ready layers and helper tables for print layouts, then export a PDF atlas from the plugin.
The current publish flow supports:
- atlas page extent planning
- cover and summary helper tables
- TOC-ready helper rows
- route-profile sample tables for layout charts
- programmatic PDF export through the atlas/export subsystem
For validation notes and rendering-sensitive workflow details, see:
docs/atlas-validation-harness.md
docs/strava-setup.mddocs/schema.mddocs/map-style-guide.mddocs/qgis-testing.md
If you are changing internals, start here:
CONTRIBUTING.mddocs/architecture.mddocs/qgis-plugin-architecture-principles.mddocs/refactoring-roadmap.mddocs/qgis-testing.mddocs/atlas-validation-harness.mdfor rendering/export-sensitive work
qfit is being evolved as a modular monolith with pragmatic ports-and-adapters boundaries.
Preferred dependency direction:
UI -> application/workflow -> domain + ports -> infrastructure adapters
In practice, that means:
- keep
qfit_dockwidget.pyfocused on UI glue - move workflow orchestration into feature-owned application modules
- keep provider-neutral logic easier to test than QGIS-heavy code
- keep QGIS, Strava, GeoPackage, settings, and PDF assembly details in infrastructure/adapters when that improves clarity
- add ports/gateways only when they earn their keep
Plugin entrypoints and UI host
qfit_plugin.pyqfit_dockwidget.pyqfit_config_dialog.pyqfit_dockwidget_base.ui
Feature-owned packages
activities/for fetch/sync/load workflows and provider-neutral activity logicanalysis/for analysis workflows, request/result shaping, and QGIS-backed analysis adaptersatlas/for publish/export workflows, runtime preparation, and PDF assemblyconfiguration/for settings, connection status, and dock-settings binding helpersproviders/for provider contracts and Strava-backed adaptersui/for dock-widget dependency assembly and UI-only coordination helpersvisualization/for render planning, basemap workflows, temporal wiring, and QGIS layer adapters
Root-level modules
The top-level Python module layer is now mostly limited to:
- plugin/bootstrap entrypoints
- a few small shared helpers such as
polyline_utils.py,time_utils.py,mapbox_config.py, andqfit_cache.py - transitional compatibility shims such as
activity_query.py,activity_classification.py,models.py, and other grandfathered modules that still exist only to cushion package migration
Rule of thumb:
Do not add new feature-specific top-level modules.
If new code belongs to one feature, it should usually live under that feature package.
- Keep thinning
QfitDockWidget. - Preserve strict feature ownership.
- Move policy into application/domain while leaving mechanics in adapters.
- Keep request/result seams explicit where they reduce UI or framework coupling.
- Delete compatibility shims once in-repo callers are migrated.
- Prefer small, reviewable PR-sized slices.
- Every behavior change needs tests.
- New workflow logic should not accumulate in
QfitDockWidget. - Prefer feature-owned modules over generic root-level helpers.
- Prefer explicit request/result dataclasses when they replace long parameter lists or messy widget-state handoff.
- Keep provider-neutral logic free of PyQGIS when practical.
- Rendering/export-sensitive changes need artifact proof, not only green CI.
activities/
activities/domain/holds provider-neutral activity logic.activities/application/owns fetch/sync/load workflows, preview helpers, and task wrappers.- GeoPackage-backed activity persistence belongs under infrastructure-oriented paths.
analysis/
- dock-facing analysis entrypoints are intentionally being thinned behind workflow-oriented application seams
- request building, dispatch, result shaping, and status policy belong in application modules, not in the dock
- QGIS-backed layer creation stays in analysis infrastructure
visualization/
- render planning, temporal intent, and user-facing visualization policy belong in
visualization/application/ - layer mutation, renderer construction, basemap loading, and QGIS project wiring belong in
visualization/infrastructure/
atlas/
atlas/owns publish/export workflows- keep naming and ownership crisp across request building, runtime preparation, task execution, and PDF assembly
- avoid adding abstraction layers that do not clarify responsibility
ui/
ui/is for dock-widget support code, dependency assembly, UI-only coordination, and similar glue- UI modules should call workflows and render results, not absorb more business logic
Run the main test suite with:
python3 -m pytest tests/ -x -q --tb=shortRun unittest discovery with:
python3 -m unittest discover -s tests -vRun the PyQGIS smoke test with:
python3 -m unittest tests.test_qgis_smoke -vOn machines without PyQGIS installed, the smoke test skips automatically.
Install qfit into a local QGIS profile for testing with:
python3 scripts/install_plugin.py --plugins-dir <QGIS plugins dir> --mode copyBuild a release-style plugin archive with:
python -m pip install pypdf
python3 scripts/package_plugin.pyThe release ZIP is written to dist/.
Before treating a change as done:
- run the relevant tests
- keep SonarCloud green
- keep CodeQL/CI green
- address meaningful review feedback
- for export/rendering work, verify the final artifact, not just object construction
If you only remember a few rules, remember these:
- make
QfitDockWidgetthinner, not heavier - keep real feature logic inside feature-owned packages
- keep QGIS-heavy mechanics out of provider-neutral workflow code
- prefer small, behavior-preserving slices
- delete migration shims once callers are gone
GPL-2.0-or-later. See LICENSE.