Namespace: s2.render ·
FFL: src/sentinel2/ffl/sentinel2_landchange.ffl (namespace s2.render) ·
Handlers: src/sentinel2/handlers/render/render_handlers.py ·
Tools: src/sentinel2/tools/_s2_tools/map_render.py ·
Tests: tests/test_sentinel2_landchange.py (test_mock_chain_end_to_end — asserts the HTML, the XYZ tiles, and change.tif)
s2.render.ChangeMap is the presentation stage: it turns the
loss(-1)/stable(0)/gain(+1) change raster from change-detection
into a real web map — a georeferenced GeoTIFF, an XYZ PNG tile pyramid, and a
MapLibre GL HTML viewer that loads those tiles over a CARTO basemap (loss in red,
gain in green). Because the change raster is method-agnostic, the same renderer
serves the difference, classify, and water methods without change.
map_render.render_change_map() has a preferred tiled path and a fallback:
- Tiled (rio-tiler present) —
_render_tiles()colorizes theint8change grid to a 3-band RGB array + a visibility mask (stable/nodata transparent), writes a deflate-compressedchange.tifviarasterio(staged in a temp dir, then published throughstorage), and slices an XYZ pyramid with amorecantileWebMercatorQuadTMS and ario_tiler.io.Reader.tile(...)per tile. Zoom range comes from_zoom_range(min zoom fits the AOI in ~1 tile; max zoom matched to the raster's native resolution, capped at 18). A_MAX_TILES=600safety cap is logged (not silent) if hit. The HTML is_maplibre_html. - Fallback (no geo stack) —
_canvas_htmlpaints the change grid to an HTML<canvas>(loss/gain/stable colored cells), fully self-contained. This is what the offline mock-test env without rasterio produces.
Either way the emitted title contains "land-cover change" and the bundle has the
same shape ({output_dir, html_path, tiles_path}), so the handler and callers don't
branch on which path ran.
Single-task — no fan-out. One render per change raster. The workflows order it
after change, the DetectChange step.
- Change codes → colors (
_COLORS): loss-1→(215,48,39)red, gain+1→(26,152,80)green;0/nodata render transparent (via the written mask). ChangeMapreturns:aoi_key,output_dir,html_path,tiles_path.- Basemap: CARTO Voyager raster (
a–dsubdomains expanded because MapLibre doesn't interpolate{s}), attribution "© OpenStreetMap © CARTO". Overridable viabasemap_url. - No filtering — it renders whatever change raster it's handed.
rio-tiler(pip,[geo]; pullsrasterio+morecantile) — GeoTIFF write, Web-Mercator reprojection, XYZ tile slicing. Optional: absent → the canvas fallback (try/except ImportErrorin_render_tiles).numpy(core dep) — the RGB/mask array construction.- MapLibre GL JS 4.7.1 — loaded in the browser from
unpkg.comby the emitted HTML (a runtime CDN dependency of the page, not a Python dep). The canvas fallback needs no JS libraries at all. - No system binaries beyond what
rasteriobundles (GDAL).
| Facet | Kind | Effect / Cost | Purpose |
|---|---|---|---|
ChangeMap(change_path, aoi_key, title="Sentinel-2 land-cover change", basemap_url="") => (aoi_key, output_dir, html_path, tiles_path) |
event | io / cheap | Render the change raster as XYZ tiles + a MapLibre HTML viewer |
Effect(kind="io") (it writes an output bundle), Cost(tier="cheap"). No
RetryPolicy (no remote reads). Callers order it after the DetectChange step —
it reads that step's raster from the cache.
- Output bundle at
output/s2/<aoi_key>/(storage.output_root()):index.html(MapLibre viewer),change.tif(georeferenced RGBA GeoTIFF, downloadable),tiles/{z}/{x}/{y}.png(XYZ pyramid). This is output, not cache — no.meta.jsonsidecar here. - On
FW_STORAGE=s3the whole bundle goes tos3://<bucket>/output/s2/…, which the dashboard's/output/rawartifact server serves directly (so a fleet run is viewable from the Runs list). The COG + every tile stage to a local temp dir and finalize throughstorage.write_bytes.
- The canvas fallback is a real, tested path — the offline suite runs without
rio-tiler and asserts
"canvas" in html. Don't assume every rendered map is tiled; a runner missing the[geo]extra silently produces the canvas view. - Tile cap is logged, never silent — hitting
_MAX_TILES=600prints which zoom was dropped ([map_render] tile cap …). Raise it (andFW_S2_MAX_SIZE) for a large, high-zoom AOI. - GDAL needs a local file — rasterio can't write/tile-read an S3 object in place,
so the COG is written to a
TemporaryDirectoryand then republished throughstorage; keep local scratch available even on an S3 backend.
- change-detection — produces the
changeraster this renders. - water-timeseries — the sibling renderer for the multi-year water viewer (reuses
map_render._BASEMAP/_zoom_range). - cache-and-storage — the
storagebackend the bundle is published through. - workflows — the entry points that end in
ChangeMap.