A trip planner for Blitz the Gap, the Canada-wide iNaturalist bioblitz: pick what counts as impact, see where a new observation adds most to what we know about Canadian biodiversity, and plan a trip to the best spot you can reach and get back from.
Live: https://pollocklab.github.io/where-to-blitz/
A companion planning tool for the Pollock Lab Blitz the Gap campaign at McGill — a planning aid, not ground truth.
- An equal-area grid of Canada: 23,214 cells of 25 km, and 536,164 of 5 km when you zoom in. Each cell is scored by a preset mix of goals.
- Explore the priority map, plan a trip (real driving/cycling/walking routes via OSRM, travel time, field time, CO₂), or compare goals side by side.
- Tap a cell to see what species to record there and which common ones are still missing ("fill the gap"), pulled live from the iNaturalist API.
- English / Canadian-French toggle; geolocation; challenge presets linked to the real Blitz the Gap iNaturalist sub-projects.
Five goals per cell, each scored 0–1; a preset blends them into a 0–100 impact score
(a percentile rank across every cell in the country, so it does not change as you pan or zoom).
All five are computed from real data, though env carries weight 0 in every shipped preset.
Does the priority actually work? On a leakage-free backtest of the 2025 BC pilot, the highest-ranked cells turn up 1.1× to 3.0× more new species than the lowest at equal effort (Spearman rho 0.40 to 0.67, permutation p < 0.001 on every taxon), and it holds out-of-sample in Eastern Canada. Mammals and reptiles are the strong cases at 3.0×; insects are the weak one, 1.3× in BC and 1.1× in the East.
METHODOLOGY.md has the glossary, the per-axis formulas and sources, the per-taxon validation numbers, and the scope those numbers hold within.
The app is a single self-contained index.html generated by build_webapp.py:
python build_webapp.py # regenerates index.html from cluster_results/ca/Edit the template webapp/index.html (or build_webapp.py), never the generated
index.html — it is overwritten on every build and on every push to main. The build is
deterministic (no timestamps/randomness) — same inputs produce a byte-identical index.html.
cluster_results/ca/index.json— group → data-file map, lattice definition, methods metadata.cluster_results/ca/webapp_data_<group>.json— per-group cell data, fetched at runtime.cluster_results/ca/ca_density_*.tif— density rasters (gitignored, regenerable via thebuild_*_ca.pyscripts).
No server, no database: Python writes files, the browser reads them. The six map layers are not the same kind of thing, which is the one non-obvious part:
| Layer | Served as | Built by |
|---|---|---|
| Base map | XYZ raster tiles (CARTO / ArcGIS / OpenTopoMap) | — |
| Cell geometry | GeoJSON polygons in the LAEA lattice | grid_lattice.py |
| Cell colours | values/*.png, one pixel per cell, painted onto those polygons |
build_grid_values.py |
| Density overlay | Raster PMTiles under density/, served same-origin |
build_density_pmtiles.py |
| Density, Fungi only | Live TiTiler over a 1 km COG on Arbutus | — |
| Climate Gap overlay | A single raster PMTiles, climate_gap.pmtiles, served same-origin |
not built here: baked from CHELSA outside the repo and uploaded to the release |
Why colours are a PNG and not tiles: build_grid_values.py docstring (#116).
Why Fungi differs: build_density_pmtiles.py docstring.
Climate Gap is the one layer no script in this repo regenerates, so the release asset is its only
source; both _site assembly jobs test -s it rather than let a missing file blank the map.
A fresh clone looks empty on the map: cluster_results/ca/*.json is in git, but tiles/ and
cluster_results/ca/values/ are gitignored. Both are release assets, and their names already match
what the app fetches, so nothing needs renaming:
gh release download grid-outputs-v1 --repo PollockLab/where-to-blitz \
--dir tiles --pattern "density_All.pmtiles" --clobber
gh release download grid-outputs-v1 --repo PollockLab/where-to-blitz \
--pattern grid_values.tar.gz --clobber
mkdir -p cluster_results/ca/values && tar -xzf grid_values.tar.gz -C cluster_results/ca/values
python build_webapp.py
npx http-server -p 8765 -c-1 # then open http://localhost:8765/That is 48 MB, about a minute. Use --pattern "*.pmtiles" for every layer (180 MB).
Serve it with something that honours Range. PMTiles reads byte ranges out of one large file.
python -m http.server ignores the Range header and answers 200 with the whole body (Python
3.11 and 3.14 both do), so pmtiles.js stops with "Server returned no content-length header or
content-length exceeding request. Check that your storage backend supports HTTP Byte Serving." The
vector grid still draws, which is what makes it confusing: only the rasters go missing. Check your
server in one line — it must print 206:
curl -s -o /dev/null -w '%{http_code}\n' -r 0-99 http://localhost:8765/tiles/density_All.pmtilesOpening index.html as a file:// URL does not work either; use the local server.
A planning aid, not a census. Sensitive and at-risk species are gated out of suggestions
(taxon_geoprivacy=open, threatened=false), and the at-risk layer is only ever exposed as a
per-cell score, never as point locations. Obscure sensitive locations and respect
Indigenous data sovereignty before any public use. The reasoning,
including the dual-use guard it implements, is in
METHODOLOGY.md.