diff --git a/CHANGELOG.md b/CHANGELOG.md index 195c9fc..9ecda3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `expression_type`, a media type identifying the dialect of `expression` (e.g. `text/x-numexpr`, + `application/vnd.maplibre.expression+json`), and a `rel: "stylesheet"` link relation for referencing external + style documents (SLD, Mapbox/MapLibre Style, OpenLayers Flatstyle, QGIS QML), sharing the same media-type + vocabulary. A stylesheet link may also address a specific named layer/style inside a multi-layer document via + a URI fragment on `href` ([#17](https://github.com/stac-extensions/render/issues/17), + [#21](https://github.com/stac-extensions/render/issues/21)) +- Add a vector data example (`examples/item-vector.json`), showing the extension used on non-raster data with + a MapLibre expression and stylesheet link +- Add an `assets` cross-reference attribute to `rel: "stylesheet"` links, so a stylesheet can be tied + directly to one or more assets without requiring a `renders` entry — useful for vector styling, where + `render` (raster-focused) often doesn't apply at all + +### Fixed + +- Require `expression` whenever `expression_type` is set (the reverse is not required, so existing + documents using `expression` alone remain valid) +- Correct the `stylesheet` link documentation: STAC Asset Objects have no `links` field, so a stylesheet + link always lives at the item/collection level, never directly on an asset + ## [2.1.0] - 2026-09-22 ### Added diff --git a/README.md b/README.md index cac4064..1520b6f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Rendering extension aims at providings consumers with the possible rendering of - Examples: - [Landsat-8 example](examples/item-landsat8.json): Shows the basic usage of the extension in a landsat-8 STAC Item - [Sentinel-2 example](examples/item-sentinel2.json): Shows the basic usage of the extension in a Sentinel-2 STAC Item + - [Vector example](examples/item-vector.json): Shows the extension used on vector data, styled with a MapLibre expression and stylesheet link - [Collection example](examples/collection.json): Shows the basic usage of the extension in a collection - [JSON Schema](json-schema/schema.json) - [Changelog](./CHANGELOG.md) @@ -44,7 +45,8 @@ The fields in the table below can be used in these parts of STAC documents: | colormap | object | [Color map JSON definition](https://developmentseed.org/titiler/user_guide/rendering/#custom-colormaps) that must be applied for a raster band | | color_formula | string | [Color formula](https://developmentseed.org/titiler/user_guide/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. | +| expression | string, object, array | Expression to derive the rendered value(s) from the referenced assets, e.g. a band-math formula or a style expression (which may also cover conditionals, interpolation, or other non-arithmetic operations). See [Expression and stylesheet formats](#expression-and-stylesheet-formats) for how to identify its dialect with `expression_type`. | +| expression_type | string | Media type identifying the dialect of `expression` (e.g. `text/x-numexpr`, `application/vnd.maplibre.expression+json`). If `expression_type` is set, `expression` MUST also be set — but not the reverse: `expression` alone remains valid. See [Expression and stylesheet formats](#expression-and-stylesheet-formats). If not set, a string `expression` SHOULD be assumed to be `text/x-numexpr` for backwards compatibility; an object or array `expression` SHOULD NOT be assumed to be any particular dialect. | | minmax_zoom | \[int] | Zoom levels range applicable for the visualization | | asset_as_band | boolean | Treat assets in `expression` as single bands. Required when expression uses multiple assets | @@ -86,6 +88,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. +## Expression and stylesheet formats + +The `render` object is intentionally implementation-agnostic: `expression` and, via +[stylesheet links](#stylesheet-links), a whole external stylesheet, can each be written in more than one +dialect (band-math strings, JSON style-expression arrays, XML style documents, ...). Without saying which +dialect a given value uses, a client has no reliable way to parse it. For example, all of the following are +different, mutually incompatible ways to express the same NDVI formula: + +- `numexpr` (used by [TiTiler](https://github.com/developmentseed/titiler)/rio-tiler): a Python-like string, + e.g. `"(B08-B04)/(B08+B04)"`. +- [MapLibre GL / Mapbox GL style expressions](https://maplibre.org/maplibre-style-spec/expressions/): a JSON + array, e.g. `["/", ["-", ["band", 2], ["band", 1]], ["+", ["band", 2], ["band", 1]]]`. +- [OpenLayers style expressions](https://openlayers.org/en/latest/apidoc/module-ol_expr_expression.html): also + JSON-array-based, but a **distinct grammar** from MapLibre/Mapbox's, despite the superficial similarity + (OpenLayers bridges to actual Mapbox/MapLibre style documents only via the separate + [`ol-mapbox-style`](https://github.com/openlayers/ol-mapbox-style) adapter package, not natively). + +None of these dialects has a formally IANA-registered media type. Where possible this extension reuses the +same informal `vnd.` media types already used by [OGC API - Styles](https://docs.ogc.org/DRAFTS/20-009.html) +for whole stylesheet documents; for expression fragments and dialects OGC API - Styles doesn't cover, this +extension defines its own, following the same `vnd.`/`x-` conventions: + +| Format | Media type | Scope | +| --- | --- | --- | +| `numexpr` band math | `text/x-numexpr` | `expression` fragment (string) | +| MapLibre/Mapbox GL style expression | `application/vnd.maplibre.expression+json` | `expression` fragment (array) | +| OpenLayers style expression | `application/vnd.openlayers.expression+json` | `expression` fragment (array) | +| Mapbox/MapLibre Style (full stylesheet) | `application/vnd.mapbox.style+json` | stylesheet document (reused from OGC API - Styles) | +| OGC Styled Layer Descriptor (SLD) | `application/vnd.ogc.sld+xml` | stylesheet document (reused from OGC API - Styles) | +| OpenLayers Flatstyle (full stylesheet) | `application/vnd.openlayers.flatstyle+json` | stylesheet document | +| QGIS QML style | `application/vnd.qgis.qml+xml` | stylesheet document | + +`expression_type` uses the "expression fragment" rows to disambiguate the `expression` field. A +[stylesheet link](#stylesheet-links)'s `type` uses the "stylesheet document" rows, since it points at a +whole, standalone style document rather than a single formula. This table is not exhaustive: additional +formats can be added following the same convention as new renderers need to be supported. + ## Dynamic tile servers integration The render objects are designed to be used by dynamic tile servers to produce RGB tiles from a STAC Item. @@ -225,6 +264,7 @@ Obviously, the same rendering can be applied to local source assets without usin "resampling": "average", "colormap_name": "ylgn", "expression": "(B08-B04)/(B08+B04)", + "expression_type": "text/x-numexpr", "rescale": [[-1,1]] } } @@ -253,6 +293,70 @@ in order to provide a cross link to the render object. } ``` +### Stylesheet links + +To reference an external, standalone style document (as opposed to the inline `expression` field), add a +link with `rel: "stylesheet"` to the item or collection's `links` array (STAC Asset Objects have no +`links` field of their own, so a stylesheet link always lives at the item/collection level, even when it +styles a single asset). The link MUST carry a `type` identifying the stylesheet's format, using the +"stylesheet document" media types from +[Expression and stylesheet formats](#expression-and-stylesheet-formats) (e.g. SLD, Mapbox/MapLibre Style, +OpenLayers Flatstyle, QGIS QML). + +A stylesheet link does not require the `render` extension. It can cross-reference what it styles two +ways, independently of each other: + +- `render`, the same attribute used by [web map links](#additional-attributes), naming which `renders` + entry it styles. Useful for fine-tuning a specific render's presentation (see the examples below). +- `assets`, an array of asset keys the stylesheet applies to directly. Useful when there is no `render` + entry at all — e.g. a vector item that has no raster pixels to rescale/colormap, only a style to + apply. `render` primarily targets raster data, so this is the expected path for vector styling. + +```json +{ + "rel": "stylesheet", + "type": "application/vnd.mapbox.style+json", + "href": "https://example.com/styles/ndvi.json", + "render": "ndvi" +} +``` + +```json +{ + "rel": "stylesheet", + "type": "application/vnd.mapbox.style+json", + "href": "https://example.com/styles/roads.json", + "assets": [ "roads" ] +} +``` + +#### Addressing a layer within a stylesheet + +A single stylesheet document can define more than one named layer or style. When that is the case, `render` +alone is not enough to disambiguate, because it names which `renders` entry the link is for, not which part +of the document to use. Append the layer/style's own name, as defined by that stylesheet format, as a URI +fragment on `href`. This reuses each format's native naming instead of inventing a new addressing scheme: + +- SLD: the ``/`` element's `` text, e.g. `styles.sld#ndvi`. +- Mapbox/MapLibre Style: a top-level `layers[].id`, e.g. `style.json#ndvi-layer`. + +```json +"links": [ + { + "rel": "stylesheet", + "type": "application/vnd.ogc.sld+xml", + "href": "https://example.com/styles/multi.sld#ndvi", + "render": "ndvi" + }, + { + "rel": "stylesheet", + "type": "application/vnd.ogc.sld+xml", + "href": "https://example.com/styles/multi.sld#sir", + "render": "sir" + } +] +``` + ## Contributing All contributions are subject to the diff --git a/examples/item-landsat8.json b/examples/item-landsat8.json index ddc2fbb..d644e53 100644 --- a/examples/item-landsat8.json +++ b/examples/item-landsat8.json @@ -115,6 +115,13 @@ "href": "https://landsat-stac.s3.amazonaws.com/collections/landsat-8-l1.json", "type": "application/json", "title": "The full collection" + }, + { + "rel": "stylesheet", + "type": "application/vnd.mapbox.style+json", + "href": "https://example.com/styles/ndvi.json", + "title": "NDVI stylesheet", + "render": "ndvi" } ], "assets": { diff --git a/examples/item-vector.json b/examples/item-vector.json new file mode 100644 index 0000000..127a766 --- /dev/null +++ b/examples/item-vector.json @@ -0,0 +1,84 @@ +{ + "type": "Feature", + "stac_version": "1.0.0", + "stac_extensions": [ + "https://stac-extensions.github.io/render/v2.1.0/schema.json" + ], + "id": "roads-example", + "bbox": [ + 2.25, + 48.82, + 2.42, + 48.9 + ], + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + 2.25, + 48.82 + ], + [ + 2.25, + 48.9 + ], + [ + 2.42, + 48.9 + ], + [ + 2.42, + 48.82 + ], + [ + 2.25, + 48.82 + ] + ] + ] + }, + "properties": { + "datetime": "2024-01-01T00:00:00Z", + "renders": { + "roads-by-class": { + "title": "Roads colored by class", + "assets": [ + "roads" + ], + "expression": [ + "match", + [ + "get", + "class" + ], + "motorway", + "#e15c5c", + "primary", + "#f2b46d", + "#cccccc" + ], + "expression_type": "application/vnd.maplibre.expression+json" + } + } + }, + "links": [ + { + "rel": "stylesheet", + "type": "application/vnd.mapbox.style+json", + "href": "https://example.com/styles/roads.json", + "title": "Roads MapLibre style", + "render": "roads-by-class" + } + ], + "assets": { + "roads": { + "title": "Road network", + "type": "application/geo+json", + "roles": [ + "data" + ], + "href": "https://example.com/data/roads.geojson" + } + } +} \ No newline at end of file diff --git a/json-schema/schema.json b/json-schema/schema.json index 4fdc47c..cdca8e6 100644 --- a/json-schema/schema.json +++ b/json-schema/schema.json @@ -65,6 +65,16 @@ } } } + }, + { + "properties": { + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/stylesheet_link" + } + } + } } ] }, @@ -128,6 +138,22 @@ } ], "definitions": { + "stylesheet_link": { + "$comment": "If a link's rel is 'stylesheet', it MUST carry a media type identifying the stylesheet format.", + "if": { + "type": "object", + "required": ["rel"], + "properties": { + "rel": { + "const": "stylesheet" + } + } + }, + "then": { + "type": "object", + "required": ["type"] + } + }, "stac_extensions": { "type": "object", "required": [ @@ -225,6 +251,12 @@ "required": [ "assets" ], + "if": { + "required": ["expression_type"] + }, + "then": { + "required": ["expression"] + }, "properties": { "assets": { "type": "array", @@ -265,6 +297,9 @@ "expression": { "type": ["string", "object", "array"] }, + "expression_type": { + "type": "string" + }, "minmax_zoom": { "type": "array", "items": {