Copernicus DEM — catalog + tile-math explorer (offline)¶
The quickstart notebook (01_dem_quickstart.ipynb) showed the end-to-end
download path against real buckets. This notebook is the offline companion:
it walks through the two curated datasets, the 1° tile convention (N##/S##
E###/W###) that names every COG in the bucket, and the pure-arithmetic helper (bbox_to_tiles) that resolves a bbox to the exact tile keys the backend will fetch — no network calls at all.
By the end you will be able to answer: "how many tiles does my bbox cost, and
which ones?" before you hit .download().
Setup¶
Everything here is offline — the catalog ships with the package and the tile math is pure arithmetic.
import pandas as pd
from earthlens.dem import Catalog
from earthlens.dem._helpers import Tile, bbox_to_tiles, tile_key, tile_name
1. The catalog — two datasets, side by side¶
The DEM catalog is small on purpose: Copernicus DEM ships two globally-
consistent grids and this backend curates both. Catalog() loads the bundled
YAML with a strict duplicate-key-rejecting loader.
| Field | Meaning |
|---|---|
key |
Value passed to EarthLens(dataset=...) |
bucket |
Anonymous AWS Open Data bucket |
resolution_token |
Fragment embedded in every tile name (_10_ / _30_) |
native_resolution_m |
Approximate pixel size |
vertical_datum |
Elevation reference (EGM2008 geoid) |
catalog = Catalog()
rows = [
{
'key': key,
'bucket': row.bucket,
'token': row.resolution_token,
'native_res_m': row.native_resolution_m,
'vertical_datum': row.vertical_datum,
}
for key, row in catalog.datasets.items()
]
pd.DataFrame(rows).set_index('key')
| bucket | token | native_res_m | vertical_datum | |
|---|---|---|---|---|
| key | ||||
| cop-dem-glo-30 | copernicus-dem-30m | 10 | 30 | EGM2008 |
| cop-dem-glo-90 | copernicus-dem-90m | 30 | 90 | EGM2008 |
2. The 1° tile-name convention¶
Copernicus DEM COGs live on a fixed integer-degree grid — one tile per 1° x 1° square, addressed by its SW corner. The name encodes:
- the resolution token:
10for GLO-30,30for GLO-90, - latitude as
N##/S##(two digits, zero-padded), - longitude as
E###/W###(three digits, zero-padded).
The _00_ markers between the coordinates are the arc-minute portion, always
00 on the 1° grid. tile_name() and tile_key() build the pieces.
examples = [
('Nile Delta (Egypt)', Tile(lat=30, lon=31), '10'),
('Amazon rainforest (Brazil)', Tile(lat=-3, lon=-60), '10'),
('Prime meridian + equator', Tile(lat=0, lon=0), '10'),
('New Zealand South Island', Tile(lat=-45, lon=170), '30'),
]
explained = [
{
'region': label,
'sw_corner': f'{tile.lat:>3d}, {tile.lon:>4d}',
'grid': 'GLO-30' if token == '10' else 'GLO-90',
'tile_identifier': tile_name(tile, token),
'bucket_key': tile_key(tile, token),
}
for label, tile, token in examples
]
pd.DataFrame(explained).set_index('region')
| sw_corner | grid | tile_identifier | bucket_key | |
|---|---|---|---|---|
| region | ||||
| Nile Delta (Egypt) | 30, 31 | GLO-30 | Copernicus_DSM_COG_10_N30_00_E031_00_DEM | Copernicus_DSM_COG_10_N30_00_E031_00_DEM/Coper... |
| Amazon rainforest (Brazil) | -3, -60 | GLO-30 | Copernicus_DSM_COG_10_S03_00_W060_00_DEM | Copernicus_DSM_COG_10_S03_00_W060_00_DEM/Coper... |
| Prime meridian + equator | 0, 0 | GLO-30 | Copernicus_DSM_COG_10_N00_00_E000_00_DEM | Copernicus_DSM_COG_10_N00_00_E000_00_DEM/Coper... |
| New Zealand South Island | -45, 170 | GLO-90 | Copernicus_DSM_COG_30_S45_00_E170_00_DEM | Copernicus_DSM_COG_30_S45_00_E170_00_DEM/Coper... |
3. From bbox to tiles — the arithmetic¶
bbox_to_tiles(lat_min, lat_max, lon_min, lon_max) snaps a bbox to the
integer-degree grid and returns every 1° tile it intersects. Pure arithmetic
— no network, no listing call.
A small bbox lying inside a single square returns one tile:
single = bbox_to_tiles(lat_min=30.2, lat_max=30.8, lon_min=31.2, lon_max=31.8)
[(t.lat, t.lon) for t in single]
[(30, 31)]
A bbox that crosses tile boundaries pulls in every neighbour. Here a small Alpine bbox reaches into four tiles in a 2x2 pattern:
alps = bbox_to_tiles(lat_min=45.4, lat_max=46.6, lon_min=7.4, lon_max=8.6)
sorted((t.lat, t.lon) for t in alps)
[(45, 7), (45, 8), (46, 7), (46, 8)]
4. Pre-flight: how big is my request?¶
Each Copernicus DEM GLO-30 tile is roughly 50 MB and each GLO-90 tile
roughly 5 MB. Combine bbox_to_tiles with the catalog to estimate a
download's size before you commit to it:
APPROX_SIZE_MB = {'10': 50.0, '30': 5.0}
def estimate_download(bbox, dataset_key: str):
row = catalog.get_dataset(dataset_key)
tiles = bbox_to_tiles(*bbox)
per_tile_mb = APPROX_SIZE_MB[row.resolution_token]
return {
'dataset': dataset_key,
'tiles': len(tiles),
'approx_MB': round(len(tiles) * per_tile_mb, 1),
}
queries = [
('France (bounding box)', (41.3, 51.1, -5.2, 9.7)),
('Egypt (bounding box)', (22.0, 32.0, 24.0, 37.0)),
('Iceland (bounding box)', (63.3, 66.6, -24.6, -13.4)),
]
rows = []
for label, bbox in queries:
for dataset in ('cop-dem-glo-30', 'cop-dem-glo-90'):
est = estimate_download(bbox, dataset)
rows.append({'region': label, **est})
pd.DataFrame(rows).set_index(['region', 'dataset'])
| tiles | approx_MB | ||
|---|---|---|---|
| region | dataset | ||
| France (bounding box) | cop-dem-glo-30 | 176 | 8800.0 |
| cop-dem-glo-90 | 176 | 880.0 | |
| Egypt (bounding box) | cop-dem-glo-30 | 130 | 6500.0 |
| cop-dem-glo-90 | 130 | 650.0 | |
| Iceland (bounding box) | cop-dem-glo-30 | 48 | 2400.0 |
| cop-dem-glo-90 | 48 | 240.0 |
What the numbers say¶
The approx_MB column is an upper bound — ocean tiles in the bbox will
come back 404 (Copernicus DEM ships no data over open ocean) and are logged
and skipped by the backend. So an Iceland bbox that intersects, say, 40 tiles
will typically download closer to 10-15 tiles once the surrounding North
Atlantic squares fall away. The real file count only lands once you call
.download().
5. Did-you-mean on typos¶
The catalog inherits the shared did-you-mean helper — a near-miss key surfaces a hint pointing at the closest curated dataset.
try:
catalog.get_dataset('cop-dem-glo-3')
except ValueError as exc:
print(exc)
'cop-dem-glo-3' is not in the DEM catalog. Known datasets: ['cop-dem-glo-30', 'cop-dem-glo-90']. Did you mean 'cop-dem-glo-30'?
Takeaway¶
- Two curated datasets — GLO-30 and GLO-90 — cover the whole Copernicus DEM addressable universe. Both are anonymous.
- Tile identifiers are deterministic. Given a bbox,
bbox_to_tilesreturns the exact tile keys the backend will fetch, before any network activity. - Antimeridian bboxes are rejected with a clear
ValueError— pass the two halves in separate calls if you need to span the 180th meridian. - Ocean tiles are absent, not fatal. Estimates from
bbox_to_tiles * ~sizeare upper bounds; the real cost is lower once ocean cells drop out.
The live end-to-end path is in 01_dem_quickstart.ipynb.