Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `minmax_resolution`, expressing the visible resolution range in `gsd` units (meters/pixel) instead of
ambiguous zoom levels, and explain the zoom-to-resolution calculation for implementations that still need
zoom levels ([#16](https://github.com/stac-extensions/render/issues/16))

### Deprecated

- `minmax_zoom` is deprecated in favor of `minmax_resolution`, since "zoom level" depends on a mapping library's
tile size convention (e.g. 256px vs 512px) and is not portable across libraries ([#16](https://github.com/stac-extensions/render/issues/16))

### Fixed

- Document that `expression` accepts `string`, `object`, or `array`, matching the schema ([#8](https://github.com/stac-extensions/render/issues/8))
Expand Down
44 changes: 42 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ The fields in the table below can be used in these parts of STAC documents:
| color_formula | string | [Color formula](https://developmentseed.org/titiler/advanced/rendering/#color-formula) that must be applied for a raster band |
| resampling | string | Resampling algorithm to apply to the referenced assets. See [GDAL resampling algorithm](https://gdal.org/programs/gdalwarp.html#cmdoption-gdalwarp-r) for some examples. |
| expression | string, object, array | Band arithmetic formula to apply to the referenced assets. The format is defined by the rendering application, e.g. a [TiTiler](https://developmentseed.org/titiler/) band math string or a [MapLibre](https://maplibre.org/maplibre-style-spec/expressions/) style expression array. |
| minmax_zoom | \[int] | Zoom levels range applicable for the visualization |
| minmax_zoom | \[int] | **Deprecated**, use `minmax_resolution` instead. Zoom levels range applicable for the visualization. Ambiguous across mapping libraries, see [Resolution vs. zoom levels](#resolution-vs-zoom-levels). |
| minmax_resolution | \[number] | Min/max ground sample distance (resolution), in the same unit as [`gsd`](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md#instrument), applicable for the visualization. Preferred over `minmax_zoom` since it is independent of any tiling scheme or library convention. |

The `render` object is open ended, so additional fields can be provided according to the needs of the rendering application.

Expand Down Expand Up @@ -85,6 +86,43 @@ It is specified as a 2 dimensions array of delimited Min,Max range per band.
A prescaling can also be performed according to the `offset` and `scale` fields value of the
[raster](https://github.com/stac-extensions/raster) extension.

## Resolution vs. zoom levels

`minmax_zoom` is **deprecated** in favor of `minmax_resolution` because "zoom level" is not an absolute unit:
it is only meaningful relative to a tiling scheme's tile pixel size and projection. The same integer zoom level
maps to a different ground resolution depending on the mapping library, because libraries disagree on the
default tile size:

- [OpenLayers](https://openlayers.org/), [Leaflet](https://leafletjs.com/) and classic XYZ/TMS tile servers
default to **256px** tiles.
- [MapLibre GL JS](https://maplibre.org/) and Mapbox GL JS default to **512px** tiles.

For a Web Mercator (EPSG:3857) tile pyramid, the resolution at a given zoom level is:

```text
resolution (m/px) = equatorial_circumference / (tile_size_px * 2^zoom)
```

where `equatorial_circumference` is ~40,075,016.6856 m. Since resolution at 512px-tile zoom `Z` equals the
resolution at 256px-tile zoom `Z + 1`, the same `minmax_zoom` value shows a different level of detail depending
on which library reads it.

`minmax_resolution` avoids this by expressing the range directly in ground resolution (the same unit as
[`gsd`](https://github.com/radiantearth/stac-spec/blob/master/item-spec/common-metadata.md#instrument), meters
per pixel), independent of any tiling scheme. A client can derive the zoom level for its own tile size with the
inverse formula:
Comment on lines +110 to +113

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reference to gsd is confusing. The gsd defintion in STAC says: "Ground Sample Distance at the sensor, in meters (m)".

If it just meant to communicate the unit, then we can just say meter. But mixing sensor and on the ground here I think doesn't help with understanding.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the usual gsd / resolution dilemma... Should we refer to the raster extension resolution field? We need a proper value to compute the min and max in the renderer

@m-mohr m-mohr Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raster:resolution is probably better suited than gsd, but not always present. We could recommend its usage here though. So an equivalent to maxResolution, minResolution in OpenLayers for example. https://openlayers.org/en/latest/apidoc/module-ol_View-View.html


```text
zoom = log2(equatorial_circumference / (tile_size_px * resolution))
```

For example, `"minmax_resolution": [10, 1000]` (10 m/px to 1000 m/px) converts to:

| tile size | minzoom | maxzoom |
| --------- | ------- | ------- |
| 256px (OpenLayers, Leaflet) | 7 | 14 |
| 512px (MapLibre GL, Mapbox GL) | 6 | 13 |

## Dynamic tile servers integration

The render objects are designed to be used by dynamic tile servers to produce RGB tiles from a STAC Item.
Expand Down Expand Up @@ -114,6 +152,7 @@ by simply specifying the `url` and `assets` query parameters.
| `color_formula` | `color_formula` | Color formula as defined in `color_formula` field of the `asset` |
| `resampling` | `resampling` | Resampling method to use when reprojecting the raster. |
| `bidx` | `bidx` | Dataset band indexes |
| `minzoom`, `maxzoom` (on the `tilejson.json` endpoint) | `minmax_resolution` | Computed from `minmax_resolution` using `zoom = log2(equatorial_circumference / (tile_size_px * resolution))`. titiler's default `WebMercatorQuad` tile matrix set uses a `tile_size_px` of 256, see [Resolution vs. zoom levels](#resolution-vs-zoom-levels). |

#### Shortwave Infra-red visual thermal signature example

Expand All @@ -127,7 +166,8 @@ From the [Sentinel-2 item](https://github.com/stac-extensions/virtual-assets/blo
"title": "Shortwave Infra-red",
"assets": [ "swir22", "nir", "red" ],
"rescale": [[0,5000],[0,7000],[0,9000]],
"resampling": "nearest"
"resampling": "nearest",
"minmax_resolution": [10, 1000]
}
}
}
Expand Down
6 changes: 5 additions & 1 deletion examples/item-sentinel2.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@
9000
]
],
"resampling": "nearest"
"resampling": "nearest",
"minmax_resolution": [
10,
1000
]
}
}
},
Expand Down
15 changes: 15 additions & 0 deletions json-schema/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@
"minmax_zoom"
]
},
{
"type": "object",
"required": [
"minmax_resolution"
]
},
{
"type": "object",
"required": [
Expand Down Expand Up @@ -265,6 +271,15 @@
"type": "number"
}
},
"minmax_resolution": {
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": {
"type": "number",
"exclusiveMinimum": 0
}
Comment thread
emmanuelmathot marked this conversation as resolved.
},
"bidx": {
"type": "array",
"items": {
Expand Down
Loading