Skip to content

Commit 6a205c4

Browse files
committed
Create QGIS Mapbox GL Style plugin
1 parent 90f304c commit 6a205c4

27 files changed

Lines changed: 864 additions & 425 deletions

.github/workflows/build.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,6 @@ jobs:
2222
with:
2323
python-version: '3.12'
2424

25-
- name: Install packaging dependencies
26-
run: python -m pip install --upgrade pypdf
27-
2825
- name: Build plugin ZIP
2926
id: package
3027
run: |
@@ -39,11 +36,11 @@ jobs:
3936
REF_NAME: ${{ github.ref_name }}
4037
run: |
4138
if [ "$EVENT_NAME" = "pull_request" ]; then
42-
echo "name=qfit-plugin-pr-${PR_NUMBER}-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
39+
echo "name=qgis-mapbox-gl-style-plugin-pr-${PR_NUMBER}-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
4340
elif [ "$REF_NAME" = "main" ]; then
44-
echo "name=qfit-plugin-main-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
41+
echo "name=qgis-mapbox-gl-style-plugin-main-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
4542
else
46-
echo "name=qfit-plugin-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
43+
echo "name=qgis-mapbox-gl-style-plugin-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
4744
fi
4845
4946
- name: Upload plugin ZIP

.github/workflows/plugin-security-scan.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
python-version: '3.12'
2323

2424
- name: Install scan dependencies
25-
run: python -m pip install --upgrade pypdf bandit detect-secrets flake8
25+
run: python -m pip install --upgrade bandit detect-secrets flake8
2626

2727
- name: Build packaged plugin and run local-style scans
2828
run: python scripts/run_plugin_security_scan.py --allow-findings

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ jobs:
2222
python-version: '3.12'
2323

2424
- name: Install test dependencies
25-
run: python -m pip install --upgrade pyyaml pypdf
25+
run: python -m pip install --upgrade pillow
2626

2727
- name: Run unit tests
2828
run: python -m unittest discover -s tests -v

README.md

Lines changed: 56 additions & 243 deletions
Original file line numberDiff line numberDiff line change
@@ -1,287 +1,100 @@
1-
# qfit
1+
# QGIS Mapbox GL Style
22

3-
Explore fitness activity data spatially in QGIS.
3+
QGIS plugin and validation harness for loading Mapbox vector tiles in QGIS and adapting Mapbox GL JS styles to the QGIS styling engine.
44

5-
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.
5+
The first target style is Mapbox Outdoors (`mapbox/outdoors-v12`). The project keeps the qfit history that built the current QGIS symbology, then continues that work in a smaller repository focused only on Mapbox vector tiles and QGIS style parity.
66

7-
![qfit screenshot in QGIS](docs/images/qfit-screenshot.png)
7+
## What It Does
88

9-
*qfit in QGIS, showing synced activity tracks and points with the plugin dock open.*
9+
- adds a QGIS action to load Mapbox Outdoors as a vector tile layer
10+
- fetches the Mapbox style JSON and sprite sheet at runtime
11+
- simplifies Mapbox GL expressions that QGIS cannot convert directly
12+
- applies the converted renderer and labeling with QGIS' `QgsMapBoxGlStyleConverter`
13+
- post-processes labels, line styles, terrain fills, road shields, contour labels, paths, and other Outdoors-specific details from the qfit work
14+
- keeps an iteration manifest so historical and future style snapshots can be rendered and compared
1015

11-
This README is split into two parts:
16+
No Mapbox token, tile payload, sprite payload, downloaded style JSON, or render output is committed.
1217

13-
1. **Part 1, for human readers** using qfit in QGIS
14-
2. **Part 2, for contributors and AI coding agents** changing the codebase
18+
## QGIS Plugin Use
1519

16-
---
20+
Install or package the plugin as `qgis_mapbox_gl_style`.
1721

18-
## Part 1, for human readers
22+
The plugin reads the Mapbox token from QGIS settings first, then from environment variables:
1923

20-
### What qfit does today
21-
22-
qfit currently supports:
23-
24-
- connecting to Strava with `client_id`, `client_secret`, and `refresh_token`
25-
- opening the Strava authorize page from the plugin and exchanging an auth code for a refresh token
26-
- fetching activities from Strava in the background
27-
- previewing fetched activities in the dock before writing anything to disk
28-
- storing a canonical local GeoPackage sync store
29-
- loading QGIS layers for tracks, start points, and optional sampled stream points
30-
- filtering by activity type, text search, date range, distance, and detailed-stream availability
31-
- applying visualization presets, optional temporal wiring, and an optional Mapbox basemap
32-
- running analysis workflows such as frequent starting points and activity heatmaps
33-
- generating atlas-ready publish layers and exporting a PDF atlas from the dock
34-
35-
### Main outputs
36-
37-
qfit uses a GeoPackage as both local sync store and QGIS data source.
38-
39-
**Internal tables**
40-
- `activity_registry`
41-
- `sync_state`
42-
43-
**Visible layers**
44-
- `activity_tracks`
45-
- `activity_starts`
46-
- `activity_points` (optional, derived from detailed streams)
47-
- `activity_atlas_pages`
48-
49-
**Atlas helper tables**
50-
- `atlas_document_summary`
51-
- `atlas_cover_highlights`
52-
- `atlas_page_detail_items`
53-
- `atlas_toc_entries`
54-
- `atlas_profile_samples`
55-
56-
### Typical workflow
57-
58-
1. Configure Strava credentials.
59-
2. Fetch activities and preview the result in the dock.
60-
3. Choose an output `.gpkg` and store the data.
61-
4. Load the qfit layers into QGIS.
62-
5. Apply visualization or analysis workflows.
63-
6. Optionally generate atlas-ready publish data and export a PDF atlas.
64-
65-
### Strava credentials
66-
67-
You need:
68-
69-
- `client_id`
70-
- `client_secret`
71-
- `refresh_token`
72-
73-
qfit includes a built-in OAuth helper in `qfit``Configuration` for the refresh-token step.
74-
75-
See:
76-
- `docs/strava-setup.md`
77-
78-
### Background maps and styling
79-
80-
qfit can load an optional Mapbox basemap and keep it below the qfit layers in the QGIS layer tree.
81-
82-
For Mapbox basemaps, raster mode is the recommended choice when you need the closest visual match to Mapbox's own rendering. Vector mode is rendered natively by QGIS and should be treated as a practical local approximation: it can be more interactive and inspectable inside QGIS, but fonts, labels, sprites, antialiasing, and zoom interpolation will not be pixel-identical to Mapbox GL JS.
83-
84-
The current visualization flow supports:
85-
86-
- semantic activity styling by activity type
87-
- simpler line-based presets
88-
- track points / start points / heatmap-oriented views
89-
- temporal timestamp wiring when timestamp fields are available
90-
91-
See:
92-
- `docs/map-style-guide.md`
93-
- `docs/mapbox-outdoors-comparison-harness.md` for manual Mapbox Outdoors browser-vs-QGIS parity checks
94-
95-
### Publish and atlas support
96-
97-
qfit can generate atlas-ready layers and helper tables for print layouts, then export a PDF atlas from the plugin.
98-
99-
The current publish flow supports:
100-
101-
- atlas page extent planning
102-
- cover and summary helper tables
103-
- TOC-ready helper rows
104-
- route-profile sample tables for layout charts
105-
- programmatic PDF export through the atlas/export subsystem
106-
107-
For validation notes and rendering-sensitive workflow details, see:
108-
- `docs/atlas-validation-harness.md`
109-
110-
### More docs
111-
112-
- `docs/strava-setup.md`
113-
- `docs/schema.md`
114-
- `docs/map-style-guide.md`
115-
- `docs/mapbox-outdoors-comparison-harness.md`
116-
- `docs/qgis-testing.md`
117-
118-
---
119-
120-
## Part 2, for contributors and AI coding agents
121-
122-
### Read these first
123-
124-
If you are changing internals, start here:
125-
126-
- `CONTRIBUTING.md`
127-
- `docs/architecture.md`
128-
- `docs/qgis-plugin-architecture-principles.md`
129-
- `docs/refactoring-roadmap.md`
130-
- `docs/qgis-testing.md`
131-
- `docs/atlas-validation-harness.md` for atlas rendering/export-sensitive work
132-
- `docs/mapbox-outdoors-comparison-harness.md` for manual Mapbox Outdoors browser-vs-QGIS visual comparison work
133-
134-
### Current architecture snapshot
135-
136-
qfit is being evolved as a **modular monolith** with pragmatic **ports-and-adapters** boundaries.
137-
138-
Preferred dependency direction:
139-
140-
```text
141-
UI -> application/workflow -> domain + ports -> infrastructure adapters
24+
```bash
25+
export QGIS_MAPBOX_GL_STYLE_MAPBOX_TOKEN="pk..."
14226
```
14327

144-
In practice, that means:
145-
146-
- keep `qfit_dockwidget.py` focused on UI glue
147-
- move workflow orchestration into feature-owned application modules
148-
- keep provider-neutral logic easier to test than QGIS-heavy code
149-
- keep QGIS, Strava, GeoPackage, settings, and PDF assembly details in infrastructure/adapters when that improves clarity
150-
- add ports/gateways only when they earn their keep
151-
152-
### Current repo shape
153-
154-
**Plugin entrypoints and UI host**
155-
- `qfit_plugin.py`
156-
- `qfit_dockwidget.py`
157-
- `qfit_config_dialog.py`
158-
- `qfit_dockwidget_base.ui`
159-
160-
**Feature-owned packages**
161-
- `activities/` for fetch/sync/load workflows and provider-neutral activity logic
162-
- `analysis/` for analysis workflows, request/result shaping, and QGIS-backed analysis adapters
163-
- `atlas/` for publish/export workflows, runtime preparation, and PDF assembly
164-
- `configuration/` for settings, connection status, and dock-settings binding helpers
165-
- `providers/` for provider contracts and Strava-backed adapters
166-
- `ui/` for dock-widget dependency assembly and UI-only coordination helpers
167-
- `visualization/` for render planning, basemap workflows, temporal wiring, and QGIS layer adapters
168-
169-
**Root-level modules**
170-
171-
The top-level Python module layer is now mostly limited to:
172-
173-
- plugin/bootstrap entrypoints
174-
- a few small shared helpers such as `polyline_utils.py`, `time_utils.py`, `mapbox_config.py`, and `qfit_cache.py`
175-
- transitional compatibility shims such as `activity_query.py`, `activity_classification.py`, `models.py`, `activity_storage.py`, and `layer_manager.py` that still exist only to cushion package migration
176-
177-
Rule of thumb:
28+
The generic `MAPBOX_ACCESS_TOKEN` variable is also supported. `QFIT_MAPBOX_ACCESS_TOKEN` remains supported only for old validation scripts and local migration continuity.
17829

179-
> Do not add new feature-specific top-level modules.
30+
In QGIS:
18031

181-
If new code belongs to one feature, it should usually live under that feature package.
32+
1. Enable the plugin.
33+
2. Open `QGIS Mapbox GL Style -> Settings`.
34+
3. Enter a Mapbox token, style owner, style id, and tile mode.
35+
4. Run `QGIS Mapbox GL Style -> Load Mapbox Outdoors`.
18236

183-
### Current architectural priorities
37+
The default style is `mapbox/outdoors-v12` in vector mode.
18438

185-
1. Keep thinning `QfitDockWidget`.
186-
2. Preserve strict feature ownership.
187-
3. Move policy into application/domain while leaving mechanics in adapters.
188-
4. Keep request/result seams explicit where they reduce UI or framework coupling.
189-
5. Delete compatibility shims once in-repo callers are migrated.
39+
## Iterations
19040

191-
### Practical coding rules
41+
Iterations are stored in `iterations/index.json`. Each entry records:
19242

193-
- Prefer small, reviewable PR-sized slices.
194-
- Every behavior change needs tests.
195-
- New workflow logic should not accumulate in `QfitDockWidget`.
196-
- Prefer feature-owned modules over generic root-level helpers.
197-
- Prefer explicit request/result dataclasses when they replace long parameter lists or messy widget-state handoff.
198-
- Keep provider-neutral logic free of PyQGIS when practical.
199-
- Rendering/export-sensitive changes need artifact proof, not only green CI.
43+
- a stable iteration id
44+
- the original qfit commit
45+
- the rewritten commit in this repository
46+
- source pull requests and issue references
47+
- the style owner/id and camera set
48+
- notes about the visual/style milestone
20049

201-
### Working conventions by area
202-
203-
**activities/**
204-
- `activities/domain/` holds provider-neutral activity logic.
205-
- `activities/application/` owns fetch/sync/load workflows, preview helpers, and task wrappers.
206-
- GeoPackage-backed activity persistence belongs under infrastructure-oriented paths.
207-
208-
**analysis/**
209-
- dock-facing analysis entrypoints are intentionally being thinned behind workflow-oriented application seams
210-
- request building, dispatch, result shaping, and status policy belong in application modules, not in the dock
211-
- QGIS-backed layer creation stays in analysis infrastructure
212-
213-
**visualization/**
214-
- render planning, temporal intent, and user-facing visualization policy belong in `visualization/application/`
215-
- layer mutation, renderer construction, basemap loading, and QGIS project wiring belong in `visualization/infrastructure/`
216-
217-
**atlas/**
218-
- `atlas/` owns publish/export workflows
219-
- keep naming and ownership crisp across request building, runtime preparation, task execution, and PDF assembly
220-
- avoid adding abstraction layers that do not clarify responsibility
221-
222-
**ui/**
223-
- `ui/` is for dock-widget support code, dependency assembly, UI-only coordination, and similar glue
224-
- UI modules should call workflows and render results, not absorb more business logic
225-
226-
### Testing
227-
228-
Run the main test suite with:
50+
List iterations:
22951

23052
```bash
231-
python3 -m pytest tests/ -x -q --tb=short
53+
python -m qgis_mapbox_gl_style.iterations list
23254
```
23355

234-
Run unittest discovery with:
56+
Render one historical iteration:
23557

23658
```bash
237-
python3 -m unittest discover -s tests -v
59+
python -m qgis_mapbox_gl_style.iterations render --iteration 006 --camera-set issue-949
23860
```
23961

240-
Run the PyQGIS smoke test with:
62+
The render command creates a detached git worktree at the selected filtered commit and runs the retained comparison harness from that snapshot. Artifacts are written under `debug/iterations/<iteration>/<timestamp>/`, which is ignored by git.
63+
64+
Use `--dry-run` to inspect the exact worktree and render command without executing QGIS/browser rendering.
65+
66+
Accept a future agent run as the next iteration:
24167

24268
```bash
243-
python3 -m unittest tests.test_qgis_smoke -v
69+
python -m qgis_mapbox_gl_style.iterations accept --from-run debug/iterations/012/20260620T120000Z --label "Improve contour label placement"
24470
```
24571

246-
On machines without PyQGIS installed, the smoke test skips automatically.
247-
248-
### Local install and packaging
72+
That appends the next manifest entry against the current repository `HEAD`.
24973

250-
Install qfit into a local QGIS profile for testing with:
74+
## Validation Harness
25175

252-
```bash
253-
python3 scripts/install_plugin.py --plugins-dir <QGIS plugins dir> --mode copy
254-
```
76+
The retained validation scripts live under `validation/` and are intentionally close to their qfit originals. Some report fields still say `qfit_*` or "qfit preprocessing" because those names are part of the historical audit output and tests.
25577

256-
Build a release-style plugin archive with:
78+
The main visual comparison harness is:
25779

25880
```bash
259-
python -m pip install pypdf
260-
python3 scripts/package_plugin.py
81+
python validation/mapbox_outdoors_comparison.py --all-cameras --output-root debug/mapbox-outdoors-comparison
26182
```
26283

263-
The release ZIP is written to `dist/`.
84+
See [docs/mapbox-outdoors-comparison-harness.md](docs/mapbox-outdoors-comparison-harness.md) for the full harness workflow.
26485

265-
### CI and review expectations
86+
## Packaging
26687

267-
Before treating a change as done:
88+
Build a QGIS install ZIP:
26889

269-
- run the relevant tests
270-
- keep SonarCloud green
271-
- keep CodeQL/CI green
272-
- address meaningful review feedback
273-
- for export/rendering work, verify the final artifact, not just object construction
274-
275-
### Short version for agents
90+
```bash
91+
python scripts/package_plugin.py
92+
```
27693

277-
If you only remember a few rules, remember these:
94+
The package is written to `dist/qgis_mapbox_gl_style-<version>.zip`.
27895

279-
- make `QfitDockWidget` thinner, not heavier
280-
- keep real feature logic inside feature-owned packages
281-
- keep QGIS-heavy mechanics out of provider-neutral workflow code
282-
- prefer small, behavior-preserving slices
283-
- delete migration shims once callers are gone
96+
## Provenance
28497

285-
## License
98+
This repository was seeded from the Mapbox/QGIS parts of `ebelo/qfit` with `git filter-repo`, so the style work remains inspectable as real git history instead of a copied final snapshot.
28699

287-
GPL-2.0-or-later. See [LICENSE](LICENSE).
100+
More detail is in [docs/provenance.md](docs/provenance.md).

__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
def classFactory(iface):
2-
"""Load qfit plugin class."""
3-
from .qfit_plugin import QfitPlugin
4-
return QfitPlugin(iface)
2+
"""Load the QGIS Mapbox GL Style plugin class."""
3+
from .plugin import QgisMapboxGlStylePlugin
4+
5+
return QgisMapboxGlStylePlugin(iface)

0 commit comments

Comments
 (0)