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 mix of goals you weight with sliders.
- 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; you blend them with sliders into a 0–100 impact score (a percentile rank across the cells shown). All five are built from real data.
Does the priority actually work? On a leakage-free backtest of the 2025 BC pilot, the highest-ranked cells turn up roughly 2–3× more new species than the lowest at equal effort, and it holds out-of-sample in Eastern Canada.
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 five 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 | — |
Why colours are a PNG and not tiles: build_grid_values.py docstring (#116).
Why Fungi differs: build_density_pmtiles.py docstring.
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.