Skip to content

About

Map tile layers of the City of Toronto's parks and green spaces - a reference overlay for the JOSM and iD OpenStreetMap editors, plus an OSM coverage-gap review page

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Toronto Parks Layer

Turns the City of Toronto Green Spaces dataset (~1,750 park-ish polygons) into map-tile layers that OpenStreetMap mappers can add to the JOSM and iD editors as a reference overlay.

Live layer and how to add it: https://skfd.github.io/toronto-parks-layer/

It is a sibling of toronto-addresses-layer and toronto-waterways-layer: the same download -> slim -> tiles -> site -> publish pipeline, pointed at the Green Spaces polygons. (The City's older "Parks" dataset is deprecated; its page redirects to Green Spaces.)

What it produces

  • Vector tiles (MVT) — interactive in iD; click a polygon to read its name, class and area_id tags.
  • Raster tiles (PNG) — translucent green fills with park-name labels; each park's outline and label share a stable hashed colour (as in the addresses layer) so a name overhanging a small polygon is still visually tied to it. A readable backdrop for JOSM. A name only renders once the polygon is a visible shape on screen (the text may overhang small polygons), so big parks are labelled from city zoom and small parkettes only when zoomed in; at the top raster zoom every named park is labelled.
  • A landing page with copy-paste "add this layer" instructions for both editors.
  • A gap-review page (/gaps/) that compares the City polygons against park areas already in OpenStreetMap and lists the gaps — City parks with no overlapping OSM area ("missing"), matched parks whose OSM name differs ("mismatch"), and matched parks tagged both leisure=park and landuse=recreation_ground ("retag") — on an interactive map and a filterable table, with OSM / iD / JOSM links per row (the JOSM link needs JOSM running with Preferences → Remote Control enabled). OSM areas are pulled from Overpass; matching is by the area the polygons really share, with landuse=allotments accepted only for the City's gardens (see src/compare.py). Its history page charts those counts over time, one point per weekly run. Matched parks whose outlines disagree are listed apart as "worth a look", most disputed first and off by default. They are prompts to compare two shapes, not gaps, so they stay out of the gap counts and the chart.

All of it is published to GitHub Pages and rebuilt weekly.

What is kept

Green Spaces classes kept by the slim filter (INCLUDE_AREA_CLASSES in src/config.py): Park, Open Green Space, Golf Course, Cemetery, Civic Centre Square and their OTHER_* variants. Traffic islands, road slivers, boulevards, hydro corridors, building grounds and the generic administrative OTHER_* classes are dropped. Names are converted from the City's ALL CAPS to title case.

The numbered TRCA LANDS ( n) parcels are kept out of the rendered tile layer (they are conservation-authority land, not real named parks). The slim step writes them to parks-slim.geojsonl but not to parks-layer.geojsonl, so the gap-review page still tracks them while the tiles do not.

Setup

  1. Install Python dependencies:
    pip install -r requirements.txt
    
  2. Set up WSL2 + tippecanoe once — see wsl-setup.md.
  3. Confirm the GitHub repo in src/config.py (GITHUB_REPO, PAGES_URL).

Usage

python run.py download   # fetch the latest Green Spaces GeoJSON (smart-cached)
python run.py slim       # filter to park-ish polygons -> slim GeoJSONL
python run.py compare    # diff City polygons against OSM -> data/gaps.geojson
python run.py backfill   # one-off: reconstruct past weeks' gap counts from OSM history
python run.py vector     # build vector (MVT) tiles via WSL tippecanoe
python run.py raster     # build labelled raster (PNG) tiles
python run.py site       # render the landing page (+ the gap-review page)
python run.py publish    # force-push the site to the gh-pages branch

python run.py build      # download + slim + compare + vector + raster + site
python run.py update     # build + publish  (the scheduled entry point)

Build output lands in build/site/; that directory is what gets published.

Tile endpoints

https://skfd.github.io/toronto-parks-layer/tiles/vector/{z}/{x}/{y}.pbf   (z10-19)
https://skfd.github.io/toronto-parks-layer/tiles/raster/{z}/{x}/{y}.png   (z13-17)

The vector layer name is parks.

Hosting

The tile pyramid is published to an orphan gh-pages branch, recreated and force-pushed on every build so repository history never grows. One-time step: in the GitHub repo, set Settings → Pages → Source to the gh-pages branch (root).

Scheduling (Windows)

Run as Administrator:

.\schedule-add.ps1      # registers a weekly task "kk-TorontoParksLayer", Mondays 16:30
.\schedule-remove.ps1   # unregisters it

Weekly is enough — the City refreshes the Green Spaces dataset monthly, so the tiles rarely change; it is the gap page, which diffs against live OSM, that moves week to week. 16:30 keeps the build clear of the daily address-layer task. The task runs python run.py update and appends output to logs\scheduler.log.

The link gate

Both network steps — the dataset download and the gh-pages push — run behind addressvault.net, the same offline/metered gate the address vault pulls behind. A dead link is not a build failure: run.py exits 75 (EX_TEMPFAIL), nothing is written, and the task's restart-on-failure retries three times half an hour apart rather than leaving a seven-day gap. It does not wait the link out, since the task carries a hard ExecutionTimeLimit that would kill a long wait anyway.

A download dropped mid-body resumes with a Range request instead of starting over, and a push that cannot resolve github.com is retried without rebuilding — the finished tiles stay on disk, so python run.py publish can complete the run later. Before this, a dead resolver on 2026-07-27 took out the run with an unhandled traceback out of the first request.

Re-run schedule-add.ps1 to apply the restart settings to an already registered task.

Gap history

Every live comparison appends one line to data/history.jsonl -- the date, the OSM data date, and the missing / mismatch / unnamed / TRCA counts -- and the site step charts it at /gaps/history.html, with the rows published beside it as gaps/history.jsonl. That is the whole record the observer keeps: the tiles and the gap list are rebuilt from scratch and force-pushed each week, but this file only grows, so the scheduled update extends the chart by one point every Monday. A run that fell back to cached OSM is not recorded; it observed nothing new, and a point it drew would be a false plateau.

Nothing heavier is kept on purpose. OSM keeps its own history, so the record is reconstructible: python run.py backfill asks overpass-api.de what OSM looked like on each past Monday (an attic query, [date:...], which only that instance is relied on for) and re-runs the comparison against it, adding rows marked attic. It is paced -- one attempt per date, 30 s between dates, and it stops at the first unanswered query -- because attic queries cost the instance more than live ones and this is a one-off, not the weekly build. Dates already on record are skipped, so an interrupted run resumes when re-run. The default range starts 2026-06-01, the week before the layer went live; --since, --until, --every and --pause adjust it.

Two caveats the page states: a reconstructed week is compared against today's City polygons (the City side is not archived; Green Spaces changes monthly and slowly), and the counts follow the City dataset, so they move when the City adds or reclassifies parks, not only when someone maps one.

A checkout without data/history.jsonl (a fresh clone, a wiped data/) seeds itself from the published gaps/history.jsonl before recording, so the record survives the machine it was made on.

Keeping the OSM half fresh

The gap page is only worth acting on if both sides of the diff are current, and the OSM side is the one that moves daily. It used to be fetched with a single POST to overpass-api.de, falling back to the cached response on any error — which meant a transient 504 cost a whole week. That happened twice running, on 2026-08-10 and 2026-08-17, and the task reported success both times while the page compared against OSM data from Aug 3.

The fetch now works for it. Every mirror in OVERPASS_URLS is tried, and the whole list is retried OVERPASS_ROUNDS times a minute apart, because what is being worked around is a loaded instance shedding one request. A reply with fewer than OSM_MIN_ELEMENTS elements is refused however healthy its status code: overpass.osm.ch serves a Switzerland extract and answers a Toronto bbox with a valid, empty result, which taken at face value would report all 1,745 City parks as missing from OSM.

Measured 2026-08-17 with the real query, from this laptop:

mirror time result
overpass-api.de 5.1s 200, 6,690 elements
overpass.private.coffee 16.9s 200, 6,577 elements
overpass.kumi.systems 32.1s 504
overpass.osm.jp 0.8s SSL failure
overpass.osm.ch 0.9s 200, 0 elements (wrong region)

A full reply is not proof of a current one either. Every Overpass reply states its own data date (osm3s.timestamp_osm_base), and one more than OSM_MAX_AGE_DAYS (3) old is refused like a 504. That check exists because overpass.private.coffee, the fallback in the table above, turned out to answer from a pool of lagging backends: on 2026-09-28 and 09-29 it served OSM as of 07-15 and 05-06, and both weeks' gap pages presented that as current. It was dropped from OVERPASS_URLS the same day, leaving overpass-api.de alone; a cached reply is likewise dated by the data it holds, not by when it was fetched.

If it still cannot reach anyone, the cache is used — a stale gap page beats none — but the run stops being quiet about it. The page prints the OSM data's own date beside the City date and carries a warning banner, and update exits 70 after publishing, so the tiles still ship and the task's restart-on-failure tries the fetch again in half an hour instead of in a week. Being offline is told apart from an Overpass outage and stays a 75.

Tests

python tests\test_tilemath.py
python tests\test_slim.py
pytest                       # the above, plus the download / publish tests

Licence / attribution

Park data is © City of Toronto, published under the Open Government Licence – Toronto. Tiles and the landing page carry that attribution.

About

Map tile layers of the City of Toronto's parks and green spaces - a reference overlay for the JOSM and iD OpenStreetMap editors, plus an OSM coverage-gap review page

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages