Context
Who's filing & why. Digital-Earth is a geospatial
visualization package built on top of pyramids: pyramids is the GIS engine (I/O, formats, reprojection,
analysis, tiling, cloud); Digital-Earth only turns pyramids objects into maps/figures. By design Digital-Earth
must not reimplement GIS or import competitor libraries (rasterio / xarray / fiona / GDAL-directly /
earthengine). So when Digital-Earth needs a GIS capability pyramids does not expose, it is filed here.
How this list was produced. While building Digital-Earth's map/web visualization tiers we audited pyramids
0.34.0 against the modern web-GIS feature surface. ~90% was already present (formats incl.
KML/GPX/FlatGeobuf/PMTiles-read, COG/XYZ tiling + overviews, reproject/resample/crop, the full vector op set,
slope/aspect/hillshade/contour/zonal, S3/GCS/Azure/STAC/WMS/WFS). The items below are the remaining gaps. The
PY-* tags are Digital-Earth's internal IDs — ignore them.
How to work this issue (please read before implementing)
- One item = one PR. This is an umbrella; do not implement all of it at once. Start with item 1
(terrain-RGB) — it is the only item that blocks already-shipped Digital-Earth code; the rest are future-facing.
- Follow pyramids' existing conventions (verified against the 0.34.0 source):
- Raster behaviour is implemented in an engine mixin under
src/pyramids/dataset/engines/ and exposed via a
thin delegator on the Dataset facade (src/pyramids/dataset/dataset.py). Tiling/I/O lives in
engines/io.py (to_xyz, get_tile, create_overviews) and engines/cog.py (to_cog); cell/grid in
engines/cell.py (get_cell_polygons); reprojection in engines/spatial.py (to_crs); vectorize in
engines/vectorize.py. Vector behaviour lives on FeatureCollection
(src/pyramids/feature/collection.py: to_parquet, to_file, …).
- Writers take
path first, then keyword-only options after *, and return Path (see to_cog). Reads
return the pyramids object. Reuse the existing kwarg vocabulary (resampling, out_dtype, nodata, bands).
- Add tests (the repo's pytest style) and a Google-style docstring with a runnable doctest matching the
existing methods (e.g. to_xyz/to_cog use a Dataset.create_from_array(...) example).
- Do not assume an external dependency is acceptable — where one is needed (routing, H3, EE) it is called out
per item; confirm the dependency policy in a comment before adding it.
Item 1 — Dataset.to_terrain_rgb(...) · PY-IO.9 · PRIORITY (blocks shipped DE code)
What. Encode a single-band elevation raster (a DEM, metres) into terrain-RGB raster tiles — elevation
packed into the R/G/B channels — so browser/GPU engines (MapLibre, deck.gl, Cesium) can render 3-D terrain.
Where it lives. New method in a raster I/O engine (alongside to_xyz/create_overviews in
src/pyramids/dataset/engines/io.py), delegated from Dataset (dataset.py) like the other to_* writers.
Signature (mirror to_cog's keyword-only style):
def to_terrain_rgb(
self,
path: str | Path,
*,
encoding: str = "mapbox", # "mapbox" (Terrain-RGB) or "terrarium" (Mapzen)
tiles: bool = True, # True -> write an XYZ {z}/{x}/{y}.png pyramid; False -> one RGB(A) raster
min_zoom: int = 0,
max_zoom: int | None = None, # default: derived from the source resolution
tile_size: int = 256,
base_val: float = -10000.0, # mapbox only: elevation that maps to RGB (0,0,0)
interval: float = 0.1, # mapbox only: metres per encoded unit
resampling: str = "bilinear", # reproject/downsample resampling
band: int = 0,
) -> Path:
Behaviour (be exact — do not infer):
- Take band
band of the source as float elevation in metres. Reproject to EPSG:3857 if not already
(reuse engines/spatial.py::to_crs).
- Encode each pixel to 8-bit R, G, B:
encoding="mapbox" (Mapbox Terrain-RGB): with v = round((height - base_val) / interval):
R = (v >> 16) & 255, G = (v >> 8) & 255, B = v & 255.
Decoder (for the round-trip test): height = base_val + (R*65536 + G*256 + B) * interval.
encoding="terrarium" (Mapzen): with v = height + 32768:
R = floor(v / 256), G = floor(v) % 256, B = floor((v - floor(v)) * 256).
Decoder: height = (R*256 + G + B/256) - 32768.
- NoData pixels: emit a fully-transparent pixel — write RGBA with
A=0 there (A=255 elsewhere). If the
source has no nodata, write RGB.
- Output:
tiles=True -> an XYZ pyramid of tile_sizextile_size PNGs under path/{z}/{x}/{y}.png, zooms
min_zoom..max_zoom (reuse the tiling path used by to_xyz/get_tile); default max_zoom from the native
pixel size.
tiles=False -> a single 3- or 4-band PNG/GeoTIFF at path.
- Return the output
Path (the tile-root dir, or the file).
Edge cases to handle (named, not assumed): values outside the encodable range must be clamped (mapbox:
v clamped to [0, 2**24 - 1]); max_zoom < min_zoom -> ValueError; a multi-band source without an explicit
band -> use band 0 and document it; a non-projected source is reprojected (do not error).
Acceptance criteria:
References: Mapbox Terrain-RGB v1 spec; Mapzen Terrarium spec; rio-rgbify is a reference encoder (do not
depend on it — implement with numpy + the existing tiler).
Item 2 — FeatureCollection.interpolate_to_raster(...) · PY-AN.15
What. Interpolate scattered point measurements into a continuous raster surface (IDW + ordinary Kriging).
FeatureCollection.interpolate today is line/1-D interpolation — this is point->grid.
Where. New method on FeatureCollection (src/pyramids/feature/collection.py); returns a Dataset.
Signature:
def interpolate_to_raster(
self, column: str, *, method: str = "idw", cell_size: float | None = None,
bounds: tuple[float, float, float, float] | None = None, power: float = 2.0,
n_neighbors: int | None = None, variogram: str = "spherical", nodata: float = -9999.0,
) -> "Dataset":
Behaviour: read column as the z-value at each point geometry; build a grid (from bounds+cell_size, else
the layer extent and a sensible default cell size); method="idw" -> inverse-distance weighting with exponent
power (optionally limited to n_neighbors); method="kriging" -> ordinary kriging with the named variogram.
Output a single-band Dataset in the layer's CRS with nodata outside the convex hull.
Edge cases: <3 points -> ValueError; non-numeric / all-NaN column -> ValueError; duplicate coordinates
-> average them. Acceptance: IDW evaluated at a sample point ~= that point's value; output CRS == input CRS;
the grid honours bounds/cell_size. Reference: classic IDW/kriging formulations; scipy / pykrige as
references — confirm dependency policy before adding pykrige.
Item 3 — Network analysis (isochrones / service areas / OD matrices) · PY-AN.17
What. Routing-based analysis. Needs a routing engine (OSRM / Valhalla / pgRouting / OSMnx) — so the first
deliverable is a design decision, not code: does pyramids wrap a routing backend, or is this out of scope?
Please comment with a decision before implementing. If in scope, suggested surface:
isochrones(points, *, minutes, profile="driving", backend=...) -> FeatureCollection (polygons) and
od_matrix(origins, destinations, *, profile) -> DataFrame. Acceptance to be defined once the backend is
chosen. Reference: OSRM /table + isochrone, Valhalla isochrone API, OSMnx.
Item 4 — H3 hexagonal grids · PY-AN.18
What. Uber H3 indexing/binning. Where. FeatureCollection. Signatures:
def to_h3(self, resolution: int) -> "FeatureCollection" # add an `h3` index column per point
def h3_bin(self, resolution: int, *, agg: str = "count", column: str | None = None) -> "FeatureCollection"
Behaviour: to_h3 adds the H3 cell index (resolution 0-15) for each point geometry. h3_bin groups points by
cell and returns one hexagon polygon per occupied cell with the aggregate (count, or agg over column).
Output CRS EPSG:4326 (H3 is lat/lng). Edge cases: non-point geometry -> ValueError; resolution outside
0-15 -> ValueError. Acceptance: cell count matches the h3 library for a known fixture; hex polygons are
valid and closed. Reference: h3-py (this needs the h3 dependency — confirm policy first).
Item 5 — Spatial statistics (autocorrelation / hotspots) · PY-AN.19
What. Global/local Moran's I, Getis-Ord Gi* hotspots, LISA clusters over a FeatureCollection column. Needs a
spatial-weights concept (contiguity / k-nearest / distance-band) — define that first. Suggested surface:
spatial_autocorrelation(column, *, weights="queen") -> dict (global I + p-value) and
hotspots(column, *, weights="queen") -> FeatureCollection (per-feature Gi* z-score + significance class).
Acceptance: results match esda / libpysal on a known fixture within tolerance. Reference: PySAL esda,
libpysal (confirm dependency policy). Scope precisely before building — this is the largest item (L).
Item 6 — Google Earth Engine reader · PY-CL.7
What. Read Earth Engine assets/collections into pyramids Dataset / DatasetCollection. Must live in
pyramids (Digital-Earth forbids xee / earthengine-api). Suggested surface: a pyramids.ee module —
pyramids.ee.read(asset_id, *, region, scale, bands=None) -> Dataset and a collection reader for image
collections. Requires earthengine-api (+ auth) — confirm dependency/auth policy before implementing.
Acceptance: a small public EE image reads into a Dataset with correct CRS/transform/bands. Reference:
earthengine-api, xee (as behaviour reference only).
Convenience wrappers (the capability already exists in GDAL/pyramids — just surface it)
Each is small (~half a day). Add method + test + docstring, following the conventions above.
PY-IO.8 — FeatureCollection.to_pmtiles(path) -> Path. GDAL's PMTiles driver already writes; wrap it like
the existing to_file / to_parquet. Acceptance: output reopens via read_file and round-trips geometry +
attributes.
PY-IO.13 — read_gpx_layers(path) -> dict[str, FeatureCollection]. The GPX driver already exposes
waypoints / tracks / routes sub-layers; return all present ones keyed by name. Acceptance: a
multi-layer GPX yields the present sub-layers; missing ones are omitted.
PY-IO.14 — vector-tile (MVT) writer. Overlaps PY-IO.8; consider folding into that PR. Needed only for
very large served vector layers.
PY-AN.8 — FeatureCollection.fishnet(bounds, cell_size, *, crs=None) -> FeatureCollection. Arbitrary-extent
vector grid of square polygons. Dataset.get_cell_polygons (engines/cell.py) already does the raster-aligned
case — this is the vector / arbitrary-bbox analogue. Acceptance: cell count == ceil(w/cs) * ceil(h/cs); all
polygons valid and non-overlapping.
PY-CL.4 — ArcGIS FeatureServer helper. The ESRIJSON driver already reads FeatureServer output; add a URL
convenience + result pagination (FeatureServer caps records per request). Acceptance: a paged layer reads all
features, not just the first page.
PY-CL.6 — Planetary Computer signing helper. STAC access works (pyramids.stac); confirm or add a
planetary-computer-style token / URL-signing helper so signed asset hrefs read via the existing readers.
Suggested order
- Item 1 (terrain-RGB) — unblocks shipped Digital-Earth code. Do this first.
- The convenience wrappers (
PY-IO.8, .13, .14, PY-AN.8, PY-CL.4, .6) — quick wins.
- Items 2, 4, 5 (interpolation, H3, spatial stats) — each its own PR; confirm any new dependency first.
- Items 3 and 6 (routing, EE) — decision-first: comment with the dependency/scope decision before coding.
Context
Who's filing & why. Digital-Earth is a geospatial
visualization package built on top of pyramids: pyramids is the GIS engine (I/O, formats, reprojection,
analysis, tiling, cloud); Digital-Earth only turns pyramids objects into maps/figures. By design Digital-Earth
must not reimplement GIS or import competitor libraries (rasterio / xarray / fiona / GDAL-directly /
earthengine). So when Digital-Earth needs a GIS capability pyramids does not expose, it is filed here.
How this list was produced. While building Digital-Earth's map/web visualization tiers we audited pyramids
0.34.0 against the modern web-GIS feature surface. ~90% was already present (formats incl.
KML/GPX/FlatGeobuf/PMTiles-read, COG/XYZ tiling + overviews, reproject/resample/crop, the full vector op set,
slope/aspect/hillshade/contour/zonal, S3/GCS/Azure/STAC/WMS/WFS). The items below are the remaining gaps. The
PY-*tags are Digital-Earth's internal IDs — ignore them.How to work this issue (please read before implementing)
(terrain-RGB) — it is the only item that blocks already-shipped Digital-Earth code; the rest are future-facing.
src/pyramids/dataset/engines/and exposed via athin delegator on the
Datasetfacade (src/pyramids/dataset/dataset.py). Tiling/I/O lives inengines/io.py(to_xyz,get_tile,create_overviews) andengines/cog.py(to_cog); cell/grid inengines/cell.py(get_cell_polygons); reprojection inengines/spatial.py(to_crs); vectorize inengines/vectorize.py. Vector behaviour lives onFeatureCollection(
src/pyramids/feature/collection.py:to_parquet,to_file, …).pathfirst, then keyword-only options after*, and returnPath(seeto_cog). Readsreturn the pyramids object. Reuse the existing kwarg vocabulary (
resampling,out_dtype,nodata,bands).existing methods (e.g.
to_xyz/to_coguse aDataset.create_from_array(...)example).per item; confirm the dependency policy in a comment before adding it.
Item 1 —
Dataset.to_terrain_rgb(...)·PY-IO.9· PRIORITY (blocks shipped DE code)What. Encode a single-band elevation raster (a DEM, metres) into terrain-RGB raster tiles — elevation
packed into the R/G/B channels — so browser/GPU engines (MapLibre, deck.gl, Cesium) can render 3-D terrain.
Where it lives. New method in a raster I/O engine (alongside
to_xyz/create_overviewsinsrc/pyramids/dataset/engines/io.py), delegated fromDataset(dataset.py) like the otherto_*writers.Signature (mirror
to_cog's keyword-only style):Behaviour (be exact — do not infer):
bandof the source as float elevation in metres. Reproject to EPSG:3857 if not already(reuse
engines/spatial.py::to_crs).encoding="mapbox"(Mapbox Terrain-RGB): withv = round((height - base_val) / interval):R = (v >> 16) & 255,G = (v >> 8) & 255,B = v & 255.Decoder (for the round-trip test):
height = base_val + (R*65536 + G*256 + B) * interval.encoding="terrarium"(Mapzen): withv = height + 32768:R = floor(v / 256),G = floor(v) % 256,B = floor((v - floor(v)) * 256).Decoder:
height = (R*256 + G + B/256) - 32768.A=0there (A=255elsewhere). If thesource has no nodata, write RGB.
tiles=True-> an XYZ pyramid oftile_sizextile_sizePNGs underpath/{z}/{x}/{y}.png, zoomsmin_zoom..max_zoom(reuse the tiling path used byto_xyz/get_tile); defaultmax_zoomfrom the nativepixel size.
tiles=False-> a single 3- or 4-band PNG/GeoTIFF atpath.Path(the tile-root dir, or the file).Edge cases to handle (named, not assumed): values outside the encodable range must be clamped (mapbox:
vclamped to[0, 2**24 - 1]);max_zoom < min_zoom->ValueError; a multi-band source without an explicitband-> use band 0 and document it; a non-projected source is reprojected (do not error).Acceptance criteria:
interval(mapbox) /1/256m (terrarium).tiles=Truewrites a valid{z}/{x}/{y}.pngXYZ layout; tiles load in a MapLibreraster-demsource.to_terrain_rgbis exposed onDataset, has a Google-style docstring + runnable doctest, and tests.References: Mapbox Terrain-RGB v1 spec; Mapzen Terrarium spec;
rio-rgbifyis a reference encoder (do notdepend on it — implement with numpy + the existing tiler).
Item 2 —
FeatureCollection.interpolate_to_raster(...)·PY-AN.15What. Interpolate scattered point measurements into a continuous raster surface (IDW + ordinary Kriging).
FeatureCollection.interpolatetoday is line/1-D interpolation — this is point->grid.Where. New method on
FeatureCollection(src/pyramids/feature/collection.py); returns aDataset.Signature:
Behaviour: read
columnas the z-value at each point geometry; build a grid (frombounds+cell_size, elsethe layer extent and a sensible default cell size);
method="idw"-> inverse-distance weighting with exponentpower(optionally limited ton_neighbors);method="kriging"-> ordinary kriging with the namedvariogram.Output a single-band
Datasetin the layer's CRS withnodataoutside the convex hull.Edge cases:
<3points ->ValueError; non-numeric / all-NaNcolumn->ValueError; duplicate coordinates-> average them. Acceptance: IDW evaluated at a sample point ~= that point's value; output CRS == input CRS;
the grid honours
bounds/cell_size. Reference: classic IDW/kriging formulations;scipy/pykrigeasreferences — confirm dependency policy before adding
pykrige.Item 3 — Network analysis (isochrones / service areas / OD matrices) ·
PY-AN.17What. Routing-based analysis. Needs a routing engine (OSRM / Valhalla / pgRouting / OSMnx) — so the first
deliverable is a design decision, not code: does pyramids wrap a routing backend, or is this out of scope?
Please comment with a decision before implementing. If in scope, suggested surface:
isochrones(points, *, minutes, profile="driving", backend=...) -> FeatureCollection(polygons) andod_matrix(origins, destinations, *, profile) -> DataFrame. Acceptance to be defined once the backend ischosen. Reference: OSRM
/table+ isochrone, Valhalla isochrone API, OSMnx.Item 4 — H3 hexagonal grids ·
PY-AN.18What. Uber H3 indexing/binning. Where.
FeatureCollection. Signatures:Behaviour:
to_h3adds the H3 cell index (resolution 0-15) for each point geometry.h3_bingroups points bycell and returns one hexagon polygon per occupied cell with the aggregate (
count, oraggovercolumn).Output CRS EPSG:4326 (H3 is lat/lng). Edge cases: non-point geometry ->
ValueError;resolutionoutside0-15 ->
ValueError. Acceptance: cell count matches theh3library for a known fixture; hex polygons arevalid and closed. Reference:
h3-py(this needs theh3dependency — confirm policy first).Item 5 — Spatial statistics (autocorrelation / hotspots) ·
PY-AN.19What. Global/local Moran's I, Getis-Ord Gi* hotspots, LISA clusters over a
FeatureCollectioncolumn. Needs aspatial-weights concept (contiguity / k-nearest / distance-band) — define that first. Suggested surface:
spatial_autocorrelation(column, *, weights="queen") -> dict(global I + p-value) andhotspots(column, *, weights="queen") -> FeatureCollection(per-feature Gi* z-score + significance class).Acceptance: results match
esda/libpysalon a known fixture within tolerance. Reference: PySALesda,libpysal(confirm dependency policy). Scope precisely before building — this is the largest item (L).Item 6 — Google Earth Engine reader ·
PY-CL.7What. Read Earth Engine assets/collections into pyramids
Dataset/DatasetCollection. Must live inpyramids (Digital-Earth forbids
xee/earthengine-api). Suggested surface: apyramids.eemodule —pyramids.ee.read(asset_id, *, region, scale, bands=None) -> Datasetand a collection reader for imagecollections. Requires
earthengine-api(+ auth) — confirm dependency/auth policy before implementing.Acceptance: a small public EE image reads into a
Datasetwith correct CRS/transform/bands. Reference:earthengine-api,xee(as behaviour reference only).Convenience wrappers (the capability already exists in GDAL/pyramids — just surface it)
Each is small (~half a day). Add method + test + docstring, following the conventions above.
PY-IO.8—FeatureCollection.to_pmtiles(path) -> Path. GDAL's PMTiles driver already writes; wrap it likethe existing
to_file/to_parquet. Acceptance: output reopens viaread_fileand round-trips geometry +attributes.
PY-IO.13—read_gpx_layers(path) -> dict[str, FeatureCollection]. The GPX driver already exposeswaypoints/tracks/routessub-layers; return all present ones keyed by name. Acceptance: amulti-layer GPX yields the present sub-layers; missing ones are omitted.
PY-IO.14— vector-tile (MVT) writer. OverlapsPY-IO.8; consider folding into that PR. Needed only forvery large served vector layers.
PY-AN.8—FeatureCollection.fishnet(bounds, cell_size, *, crs=None) -> FeatureCollection. Arbitrary-extentvector grid of square polygons.
Dataset.get_cell_polygons(engines/cell.py) already does the raster-alignedcase — this is the vector / arbitrary-bbox analogue. Acceptance: cell count ==
ceil(w/cs) * ceil(h/cs); allpolygons valid and non-overlapping.
PY-CL.4— ArcGIS FeatureServer helper. The ESRIJSON driver already reads FeatureServer output; add a URLconvenience + result pagination (FeatureServer caps records per request). Acceptance: a paged layer reads all
features, not just the first page.
PY-CL.6— Planetary Computer signing helper. STAC access works (pyramids.stac); confirm or add aplanetary-computer-style token / URL-signing helper so signed asset hrefs read via the existing readers.Suggested order
PY-IO.8,.13,.14,PY-AN.8,PY-CL.4,.6) — quick wins.