From e8519032649016c47628379e1770430bdd8dc7d6 Mon Sep 17 00:00:00 2001 From: Matthias Mohr Date: Tue, 29 Sep 2026 18:08:34 +0200 Subject: [PATCH] Add maps proposal --- README.md | 189 ++++++++++++++++++++++++- examples/collection.json | 55 ++++++-- examples/item.json | 186 +++++++++++++++++++++--- json-schema/schema.json | 297 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 694 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index a08b0f1..935749d 100644 --- a/README.md +++ b/README.md @@ -9,17 +9,198 @@ This document explains the Maps Extension to the [SpatioTemporal Asset Catalog](https://github.com/radiantearth/stac-spec) (STAC) specification. -It describes maps that combine several layers into a single view. +A map combines several layers into a single view with a projection and a default extent. +The layers can come from assets, web map links or renders, +and a stylesheet or a render can be applied to the data of an asset or a service. +Multiple maps can be provided, e.g. to offer different visualizations of the same data. + +This extension doesn't depend on other extensions. +It uses the web map links of the [Web Map Links Extension](https://github.com/stac-extensions/web-map-links) +and the renders of the [Render Extension](https://github.com/stac-extensions/render) if a document provides them. - Examples: - - [Item example](examples/item.json): Shows the basic usage of the extension in a STAC Item - - [Collection example](examples/collection.json): Shows the basic usage of the extension in a STAC Collection + - [Item example](examples/item.json): Shows maps that combine assets, web map links, renders and stylesheets in a STAC Item + - [Collection example](examples/collection.json): Shows a map with a basemap and a web map link in a STAC Collection - [JSON Schema](json-schema/schema.json) - [Changelog](./CHANGELOG.md) ## Fields -The fields of this extension are yet to be defined. +The fields in the table below can be used in these parts of STAC documents: + +- [x] Catalogs +- [x] Collections +- [x] Item Properties +- [ ] Assets (for both Collections and Items, incl. Item Asset Definitions in Collections and Asset Templates) +- [x] Links (incl. Link Templates), only [`id`](#link-identifiers) +- [ ] Bands + +| Field Name | Type | Description | +| ---------- | ---------------------------------------- | ----------- | +| maps | Map\ | **REQUIRED**. Named maps. The keys SHOULD match `^[A-Za-z0-9_-]+$`. | + +In Catalogs and Collections, `maps` is a top-level field. In Items, it's part of the `properties`. + +### Map Object + +| Field Name | Type | Description | +| ------------- | ----------------------------------- | ----------- | +| title | string | A title for the map, e.g. for a map picker. | +| description | string | A description of the map. [CommonMark 0.29](https://commonmark.org/) syntax MAY be used for rich text representation. | +| roles | \[string] | `default` marks the map that clients show by default. | +| proj:code | string\|null | The projection of the map, as defined in the [Projection Extension](https://github.com/stac-extensions/projection). | +| proj:wkt2 | string\|null | The projection of the map, as defined in the [Projection Extension](https://github.com/stac-extensions/projection). | +| proj:projjson | object\|null | The projection of the map, as defined in the [Projection Extension](https://github.com/stac-extensions/projection). | +| bbox | \[number] | The default extent of the map in WGS 84 (EPSG:4326), with 4 or 6 numbers as for the `bbox` of Items. | +| dimensions | Map\ | Default values for the non-spatial dimensions of all layers (e.g. a common `time`). | +| layers | \[[Layer Object](#layer-object)] | **REQUIRED**. The layers of the map, from bottom to top. Can't be empty. | + +If no projection is provided, clients choose one, e.g. Web Mercator. +If multiple projection fields are provided, they MUST describe the same projection. + +If no `bbox` is provided, clients use the spatial extent of the STAC entity. + +**dimensions**: The keys are the names of the dimensions, as used in the [Datacube Extension](https://github.com/stac-extensions/datacube) +(`cube:dimensions`) or by the services (e.g. `TIME` in WMS). The values apply wherever a layer's source doesn't set its own value. + +### Layer Object + +A layer has exactly one **source** and optionally a **style**. + +| Field Name | Type | Description | +| -------------- | ------------------------------------------------------------- | ----------- | +| asset | string | Source: The key of an asset. | +| link | string\|Link Object\|Link Template Object | Source: The [`id`](#link-identifiers) of a link or link template in the document, or an embedded link for layers from outside of the document (e.g. a basemap). | +| render | string | Source: The key of a render. The render provides its own data and portrayal. | +| style | [Style Reference Object](#style-reference-object) | The portrayal that is applied to the data of an `asset` or `link` source. | +| title | string | A title for the layer, e.g. for a layer switcher. Overrides the title of the source. | +| opacity | number | The opacity of the layer, between 0 (transparent) and 1 (opaque). Defaults to `1`. | +| visible | boolean | Whether the layer is visible when the map loads. Defaults to `true`. | +| min_resolution | number | The finest resolution at which the layer is shown, see [resolution ranges](#resolution-ranges). | +| max_resolution | number | The coarsest resolution at which the layer is shown, see [resolution ranges](#resolution-ranges). | + +Each layer MUST provide exactly one of `asset`, `link` and `render`. +The keys and `id`s are looked up in the document first and then in the parent Collection. +For example, an Item can use a render that its Collection defines for the corresponding `item_assets`. + +**link**: Web map links from the document are referenced by their `id`, which avoids duplicating them. +Embedding a link is meant for layers that are not part of the document, e.g. a basemap from a third party. +Embedded links follow the definitions of the web map link in use, +e.g. from the [Web Map Links Extension](https://github.com/stac-extensions/web-map-links). + +**style**: + +- It MUST NOT be provided for a `render` source, as a render has its own portrayal. +- For a `link` source, it can only be provided for services that deliver data instead of images, + e.g. vector tiles through XYZ, TileJSON or PMTiles. + +**min_resolution** and **max_resolution**: If the source of the layer is a link that indicates a resolution range itself, +clients only show the layer where both ranges allow it. + +### Style Reference Object + +| Field Name | Type | Description | +| ---------- | ------------------- | ----------- | +| link | string\|Link Object | The `id` of a stylesheet link in the document, or an embedded stylesheet link. | +| render | string | The key of a render that is applied to the data of the layer's source. | + +Each Style Reference Object MUST provide exactly one of `link` and `render`. + +**link**: Stylesheet links use the relation type `stylesheet`, which is +[registered by IANA](https://www.iana.org/assignments/link-relations/link-relations.xhtml). +The `type` is REQUIRED and identifies the encoding of the stylesheet, preferably with the media types of +[OGC API - Styles](https://docs.ogc.org/DRAFTS/20-009.html), e.g. `application/vnd.mapbox.style+json` or `application/vnd.ogc.sld+xml`. + +**render**: Only renders that don't reference any data themselves can be applied to another source. + +### Possible combinations + +The following table shows the possible combinations of sources and styles: + +| Layer | Shows | +| ----------------------------------------------------------------- | ----- | +| `{"asset": "visual"}` | The asset with the default portrayal of the client, e.g. an RGB image or a default vector style. | +| `{"asset": "fields", "style": {"link": "fields-outline"}}` | The asset, styled by a stylesheet. | +| `{"asset": "fields", "style": {"render": "fields"}}` | The asset, styled by a render. | +| `{"render": "ndvi"}` | A render with its own data, rendered by the client or delivered by a web map link that references the render. | +| `{"link": "topo-wms"}` | A service as it's delivered. | +| `{"link": "fields-pmtiles", "style": {"link": "fields-outline"}}` | The data of a service, styled by a stylesheet. | +| `{"link": "fields-pmtiles", "style": {"render": "fields"}}` | The data of a service, styled by a render. | +| `{"link": {"rel": "xyz", "href": "…"}}` | A layer from outside of the document. | + +### Resolving layers + +Clients resolve each layer as follows: + +1. `asset`: Read the asset. Apply the `style` if provided, otherwise use the default portrayal of the client. +2. `link`: Load the service. Apply the `style` to its data if provided. +3. `render`: If the client supports the render and can read its data, render it on the client. + Otherwise, use a web map link that indicates that it delivers the render (e.g. through the field `render`) + and that the client supports. If multiple links match, they are equivalent and the client can choose any of them. +4. If none of the above works, skip the layer and inform the user. + +Clients that don't support this extension usually fall back to showing the assets, renders or web map links directly. + +### Resolution ranges + +`min_resolution` and `max_resolution` define the range of resolutions in which a layer is shown. +Clients SHOULD hide the layer outside of this range. +If both are provided, `min_resolution` must be lower than or equal to `max_resolution`. + +The resolution is the **distance on the ground in meters per display (CSS) pixel**, measured at the center of the +spatial extent of the STAC entity (`bbox` in Items, the first bounding box of the spatial extent in Collections). +This makes the value independent of the projection of the map. +Clients convert the value into the units of the map projection, e.g. with `getPointResolution` in OpenLayers. + +For example, a `max_resolution` of `1000` for data centered at 48° North converts to: + +| Map projection | Resolution in map units | +| ------------------------- | ----------------------- | +| EPSG:25832 (UTM zone 32N) | ≈ 1000 m per pixel | +| EPSG:3857 (Web Mercator) | ≈ 1000 / cos(48°) ≈ 1494 m per pixel | +| EPSG:4326 (WGS 84) | ≈ 0.0134° per pixel horizontally, ≈ 0.0090° per pixel vertically | + +The ground resolution varies over large extents, so the range is approximate far from the center. +Clients MAY evaluate it at the center of the current view instead, +which is also the fallback for Catalogs and other cases where no spatial extent is available. + +The data-side counterpart is `raster:spatial_resolution` of the [Raster Extension](https://github.com/stac-extensions/raster), +which is also measured in meters on the ground. +For example, a layer with categorical data may set its `min_resolution` to the `raster:spatial_resolution` +so that it's not shown beyond the resolution of the data. +`gsd` is not suitable to derive the range as it describes the sensor, not the pixels of the data. + +## Link identifiers + +This extension adds the following field to the +[Link Object](https://github.com/radiantearth/stac-spec/tree/master/commons/links.md#link-object) +and the Link Template Object of the [Link Templates Extension](https://github.com/stac-extensions/link-templates): + +| Field Name | Type | Description | +| ---------- | ------ | ----------- | +| id | string | An identifier for the link, which MUST be unique across all `links` and `linkTemplates` of the document. **REQUIRED** for links that are referenced by a map. | + +Links are never referenced by their position in the array, +as the order can change, e.g. when a STAC API adds or regenerates links such as `self`, `root` or `parent`. + +## Relation to other specifications + +- [Render Extension](https://github.com/stac-extensions/render): + A render describes how data is portrayed. A map decides which renders are shown together, and where. + Visibility ranges are therefore defined for the map layers, not for renders. +- [Web Map Links Extension](https://github.com/stac-extensions/web-map-links): + Web map links describe services. A map references them as layers by their `id`. + A web map link can indicate that it delivers a render (e.g. through the field `render`), + so that clients can choose between rendering on the client and using the service. +- [Link Templates Extension](https://github.com/stac-extensions/link-templates): + Link templates are referenced by their `id` like links. +- [Projection Extension](https://github.com/stac-extensions/projection): + The fields `proj:code`, `proj:wkt2` and `proj:projjson` are used to define the projection of a map. +- [OGC API - Maps](https://docs.ogc.org/DRAFTS/20-058.html): + The map resources of OGC API - Maps (e.g. `/map` and `/styles/{styleId}/map`) are services that deliver images. + They are described as web map links and can be used as layers in the maps of this extension. +- [OGC API - Styles](https://docs.ogc.org/DRAFTS/20-009.html): + Stylesheet links follow OGC API - Styles, i.e. the relation type `stylesheet` and the media types for the encodings. ## Contributing diff --git a/examples/collection.json b/examples/collection.json index c6a1462..eb15770 100644 --- a/examples/collection.json +++ b/examples/collection.json @@ -1,33 +1,60 @@ { "stac_version": "1.1.0", "stac_extensions": [ - "https://stac-extensions.github.io/maps/v1.0.0/schema.json" + "https://stac-extensions.github.io/maps/v1.0.0/schema.json", + "https://stac-extensions.github.io/web-map-links/v1.3.0/schema.json" ], "type": "Collection", "id": "collection", - "title": "A title", - "description": "A description", - "license": "Apache-2.0", + "title": "Yearly mosaics", + "description": "Cloud-free yearly mosaics", + "license": "CC-BY-4.0", "extent": { "spatial": { "bbox": [ [ - 172.9, - 1.3, - 173, - 1.4 + 9, + 47.7, + 10.5, + 48.7 ] ] }, "temporal": { "interval": [ [ - "2015-06-23T00:00:00Z", + "2015-01-01T00:00:00Z", null ] ] } }, + "maps": { + "mosaic": { + "title": "Latest mosaic over a basemap", + "roles": [ + "default" + ], + "proj:code": "EPSG:3857", + "dimensions": { + "time": "2025" + }, + "layers": [ + { + "link": { + "rel": "xyz", + "href": "https://basemap.example.com/{z}/{x}/{y}.png", + "type": "image/png", + "title": "Basemap" + } + }, + { + "link": "mosaic-wmts", + "opacity": 0.9 + } + ] + } + }, "links": [ { "href": "https://example.com/examples/collection.json", @@ -36,6 +63,14 @@ { "href": "https://example.com/examples/item.json", "rel": "item" + }, + { + "id": "mosaic-wmts", + "rel": "wmts", + "href": "https://maps.example.com/wmts", + "type": "image/png", + "title": "Yearly mosaic", + "wmts:layer": "mosaic" } ] -} +} \ No newline at end of file diff --git a/examples/item.json b/examples/item.json index cc3f885..e86ffd5 100644 --- a/examples/item.json +++ b/examples/item.json @@ -1,55 +1,203 @@ { "stac_version": "1.1.0", "stac_extensions": [ - "https://stac-extensions.github.io/maps/v1.0.0/schema.json" + "https://stac-extensions.github.io/maps/v1.0.0/schema.json", + "https://stac-extensions.github.io/render/v2.1.0/schema.json", + "https://stac-extensions.github.io/web-map-links/v1.3.0/schema.json" ], "type": "Feature", "id": "item", "bbox": [ - 172.9, - 1.3, - 173, - 1.4 + 9, + 47.7, + 10.5, + 48.7 ], "geometry": { "type": "Polygon", "coordinates": [ [ [ - 172.9, - 1.3 + 9, + 47.7 ], [ - 173, - 1.3 + 10.5, + 47.7 ], [ - 173, - 1.4 + 10.5, + 48.7 ], [ - 172.9, - 1.4 + 9, + 48.7 ], [ - 172.9, - 1.3 + 9, + 47.7 ] ] ] }, "properties": { - "datetime": "2020-12-11T22:38:32Z" + "datetime": "2026-07-04T10:30:00Z", + "renders": { + "ndvi": { + "title": "NDVI", + "assets": [ + "ndvi" + ], + "rescale": [ + [ + -1, + 1 + ] + ], + "colormap_name": "rdylgn" + }, + "scene-classification": { + "title": "Scene classification", + "assets": [ + "scl" + ], + "resampling": "nearest" + } + }, + "maps": { + "overview": { + "title": "True color with field boundaries", + "roles": [ + "default" + ], + "layers": [ + { + "link": { + "rel": "xyz", + "href": "https://basemap.example.com/{z}/{x}/{y}.png", + "type": "image/png", + "title": "Basemap" + } + }, + { + "asset": "visual" + }, + { + "asset": "fields", + "style": { + "link": "fields-outline" + }, + "opacity": 0.8 + } + ] + }, + "vegetation": { + "title": "Vegetation", + "proj:code": "EPSG:25832", + "bbox": [ + 9, + 47.7, + 10.5, + 48.7 + ], + "layers": [ + { + "link": "topo-wms" + }, + { + "render": "ndvi", + "max_resolution": 1000 + }, + { + "render": "scene-classification", + "min_resolution": 20, + "visible": false + }, + { + "link": "fields-pmtiles", + "style": { + "link": "fields-outline" + }, + "visible": false + } + ] + } + } }, "links": [ { "href": "https://example.com/examples/item.json", "rel": "self" + }, + { + "id": "topo-wms", + "rel": "wms", + "href": "https://maps.example.com/wms", + "type": "image/png", + "title": "Topographic map", + "wms:layers": [ + "topo" + ] + }, + { + "id": "ndvi-wmts", + "rel": "wmts", + "href": "https://maps.example.com/wmts", + "type": "image/png", + "title": "NDVI", + "wmts:layer": "s2-ndvi", + "render": "ndvi" + }, + { + "id": "fields-pmtiles", + "rel": "pmtiles", + "href": "https://example.com/examples/fields.pmtiles", + "type": "application/vnd.pmtiles", + "title": "Field boundaries", + "pmtiles:layers": [ + "fields" + ] + }, + { + "id": "fields-outline", + "rel": "stylesheet", + "href": "https://example.com/styles/fields-outline.json", + "type": "application/vnd.mapbox.style+json", + "title": "Field outlines" } ], "assets": { - "data": { - "href": "https://example.com/examples/file.tif" + "visual": { + "href": "https://example.com/examples/visual.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "visual" + ], + "title": "True color image" + }, + "ndvi": { + "href": "https://example.com/examples/ndvi.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "title": "NDVI" + }, + "scl": { + "href": "https://example.com/examples/scl.tif", + "type": "image/tiff; application=geotiff; profile=cloud-optimized", + "roles": [ + "data" + ], + "title": "Scene classification" + }, + "fields": { + "href": "https://example.com/examples/fields.parquet", + "type": "application/vnd.apache.parquet", + "roles": [ + "data" + ], + "title": "Field boundaries" } } -} +} \ No newline at end of file diff --git a/json-schema/schema.json b/json-schema/schema.json index df64674..807398c 100644 --- a/json-schema/schema.json +++ b/json-schema/schema.json @@ -22,6 +22,303 @@ "Collection", "Feature" ] + }, + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/link_id" + } + }, + "linkTemplates": { + "type": "array", + "items": { + "$ref": "#/definitions/link_id" + } + } + }, + "oneOf": [ + { + "$comment": "Items", + "type": "object", + "required": [ + "type", + "properties" + ], + "properties": { + "type": { + "const": "Feature" + }, + "properties": { + "$ref": "#/definitions/require_maps" + } + } + }, + { + "$comment": "Catalogs and Collections", + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "enum": [ + "Catalog", + "Collection" + ] + } + }, + "allOf": [ + { + "$ref": "#/definitions/require_maps" + } + ] + } + ], + "definitions": { + "require_maps": { + "type": "object", + "required": [ + "maps" + ], + "properties": { + "maps": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/definitions/map" + } + } + } + }, + "map": { + "type": "object", + "required": [ + "layers" + ], + "properties": { + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "proj:code": { + "type": [ + "string", + "null" + ] + }, + "proj:wkt2": { + "type": [ + "string", + "null" + ] + }, + "proj:projjson": { + "type": [ + "object", + "null" + ] + }, + "bbox": { + "type": "array", + "oneOf": [ + { + "minItems": 4, + "maxItems": 4 + }, + { + "minItems": 6, + "maxItems": 6 + } + ], + "items": { + "type": "number" + } + }, + "dimensions": { + "type": "object" + }, + "layers": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/layer" + } + } + } + }, + "layer": { + "type": "object", + "oneOf": [ + { + "required": [ + "asset" + ] + }, + { + "required": [ + "link" + ] + }, + { + "required": [ + "render" + ] + } + ], + "if": { + "required": [ + "render" + ] + }, + "then": { + "not": { + "required": [ + "style" + ] + } + }, + "properties": { + "asset": { + "type": "string", + "minLength": 1 + }, + "link": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "$ref": "#/definitions/embedded_link" + } + ] + }, + "render": { + "type": "string", + "minLength": 1 + }, + "style": { + "$ref": "#/definitions/style" + }, + "title": { + "type": "string" + }, + "opacity": { + "type": "number", + "minimum": 0, + "maximum": 1 + }, + "visible": { + "type": "boolean" + }, + "min_resolution": { + "type": "number", + "exclusiveMinimum": 0 + }, + "max_resolution": { + "type": "number", + "exclusiveMinimum": 0 + } + } + }, + "style": { + "type": "object", + "oneOf": [ + { + "required": [ + "link" + ] + }, + { + "required": [ + "render" + ] + } + ], + "properties": { + "link": { + "oneOf": [ + { + "type": "string", + "minLength": 1 + }, + { + "allOf": [ + { + "$ref": "#/definitions/embedded_link" + }, + { + "required": [ + "type" + ], + "properties": { + "rel": { + "const": "stylesheet" + }, + "type": { + "type": "string", + "minLength": 1 + } + } + } + ] + } + ] + }, + "render": { + "type": "string", + "minLength": 1 + } + } + }, + "embedded_link": { + "type": "object", + "required": [ + "rel" + ], + "oneOf": [ + { + "required": [ + "href" + ] + }, + { + "required": [ + "uriTemplate" + ] + } + ], + "properties": { + "rel": { + "type": "string", + "minLength": 1 + }, + "href": { + "type": "string", + "minLength": 1 + }, + "uriTemplate": { + "type": "string", + "minLength": 1 + } + } + }, + "link_id": { + "type": "object", + "properties": { + "id": { + "type": "string", + "minLength": 1 + } + } } } }