diff --git a/.circleci/rc.yaml b/.circleci/rc.yaml index 625a856a..b0dd538c 100644 --- a/.circleci/rc.yaml +++ b/.circleci/rc.yaml @@ -4,17 +4,15 @@ plugins: # Apply some recommended defaults for consistency - remark-preset-lint-consistent - remark-preset-lint-recommended -# No HTML for security - can't activate yet due to STAC logo in README.md -# - lint-no-html + - lint-no-html # General formatting -# - - remark-lint-emphasis-marker -# - '*' + - - remark-lint-emphasis-marker + - '*' - remark-lint-hard-break-spaces - remark-lint-blockquote-indentation - remark-lint-no-consecutive-blank-lines -# Detect overly long lines - be liberal for now and don't restrict to 80 yet -# - - remark-lint-maximum-line-length -# - 150 + - - remark-lint-maximum-line-length + - 150 # Code - remark-lint-fenced-code-flag - remark-lint-fenced-code-marker @@ -23,24 +21,23 @@ plugins: - 'fenced' # Headings - remark-lint-heading-increment - - remark-lint-no-duplicate-headings - remark-lint-no-multiple-toplevel-headings - remark-lint-no-heading-punctuation - - remark-lint-maximum-heading-length - 70 - - remark-lint-heading-style - atx + - - remark-lint-no-shortcut-reference-link + - false # Lists - remark-lint-list-item-bullet-indent - remark-lint-ordered-list-marker-style - remark-lint-ordered-list-marker-value - remark-lint-checkbox-character-style -# - - remark-lint-unordered-list-marker-style -# - '-' + - - remark-lint-unordered-list-marker-style + - '-' - - remark-lint-list-item-indent - space # Tables - remark-lint-table-pipes -# - remark-lint-table-pipe-alignment # Wait for https://github.com/remarkjs/remark-lint/issues/226 -# Urls - remark-lint-no-literal-urls \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1e377a..0816a5ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added recommendation to enable CORS for public APIs ### Changed +- Added Filter extension to integrate OAFeat Part 3 CQL - Passing the `ids` parameter to an item search does not deactivate other query parameters [#125](https://github.com/radiantearth/stac-api-spec/pull/125) - The first extent in a Collection is always the overall extent, followed by more specific extents. [opengeospatial/ogcapi-features#520](https://github.com/opengeospatial/ogcapi-features/pull/520) ### Deprecated +- Query extension is now deprecated. Replaced by the Filter extension using OGC CQL ### Removed diff --git a/core/README.md b/core/README.md index 4523f275..118ec657 100644 --- a/core/README.md +++ b/core/README.md @@ -10,7 +10,8 @@ - **Extension [Maturity Classification](../extensions.md#extension-maturity):** Pilot - **Dependencies**: None -The base of a STAC API is its landing page. This resource is the starting point to discover what behaviors the API supports via the `conformsTo` values and link relations. +The base of a STAC API is its landing page. This resource is the starting point to discover what behaviors +the API supports via the `conformsTo` values and link relations. This behavior in a RESTful API is known as [Hypermedia as the Engine of Application State (HATEOAS)](https://en.wikipedia.org/wiki/HATEOAS). STAC API relies heavily on hypermedia for API resource navigation. @@ -29,7 +30,8 @@ client must inspect the the `rel` (relationship) to understand what capabilities Note the `conformsTo` JSON object follows exactly the structure of OGC API - Features [declaration of conformance classes](http://docs.opengeospatial.org/is/17-069r3/17-069r3.html#_declaration_of_conformance_classes), except is available directly under the landing page. This is a slight break from how OGC API does things, as STAC feels it is important for clients to be able to understand -conformance in a single request. Implementers choosing to also implement the OGC API - Features and/or STAC API - Features conformance classes must also implment the `/conformance` endpoint. +conformance in a single request. Implementers choosing to also implement the OGC API - Features and/or +STAC API - Features conformance classes must also implment the `/conformance` endpoint. This particular catalog provides the ability to browse down to child STAC Collection objects through its `child` links, and also provides the search endpoint to be able to search across items in its collections. Note though that none of those links are required, other servers may provide @@ -47,13 +49,13 @@ API endpoints from OAFeat or STAC API to be implemented, so the following links When implementing the STAC API Core conformance class, it it recommended to implement these Link relations. -| **`rel`** | **href to** | **From** | **Description** | -|-----------|--------------------------------------------|--------------------|------------------------------------------------------------------| -| `root` | The root URI | STAC Core | Reference to self URI | -| `self` | The root URI | OAFeat | Reference to self URI | -| `service-desc` | The OpenAPI service description | OAFeat OpenAPI | Uses the `application/vnd.oai.openapi+json;version=3.0` media type to refer to the OpenAPI 3.0 document that defines the service's API | -| `service-doc` | An HTML service description | OAFeat OpenAPI | Uses the `text/html` media type to refer to a human-consumable description of the service | -| `child` | The child STAC Catalogs & Collections | STAC Core | Provides curated paths to get to STAC Collection and Item objects | +| **`rel`** | **href to** | **From** | **Description** | +| -------------- | ------------------------------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `root` | The root URI | STAC Core | Reference to self URI | +| `self` | The root URI | OAFeat | Reference to self URI | +| `service-desc` | The OpenAPI service description | OAFeat OpenAPI | Uses the `application/vnd.oai.openapi+json;version=3.0` media type to refer to the OpenAPI 3.0 document that defines the service's API | +| `service-doc` | An HTML service description | OAFeat OpenAPI | Uses the `text/html` media type to refer to a human-consumable description of the service | +| `child` | The child STAC Catalogs & Collections | STAC Core | Provides curated paths to get to STAC Collection and Item objects | It is also valid to have `item` links from the landing page, but most STAC API services are used to serve up a large number of features, so they typically diff --git a/extensions.md b/extensions.md index dcc6a7a2..efe5276a 100644 --- a/extensions.md +++ b/extensions.md @@ -50,28 +50,31 @@ are scoped against ogcapi-features*. This is the list of all extensions that are contained in the stac-api-spec repository. -| Extension Name | Scope* | Description | Maturity | -|-----------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------|------------| -| [Fields](item-search/README.md#fields) | [Item Search](item-search/) request | Adds parameter to control which fields are returned in the response. | *Pilot* | -| [Query](item-search/README.md#query) | [Item Search](item-search/) request | Adds parameter to search Item and Collection properties. | *Pilot* | +| Extension Name | Scope* | Description | Maturity | +|----------------|--------|-------------|----------| +| [Fields](item-search/README.md#fields) | [Item Search](item-search/) request | Adds parameter to control which fields are returned in the response. | *Pilot* | +| [Filter](item-search/README.md#filter) | [Item Search](item-search/) and [STAC - Features API](ogcapi-features) `/items` requests | Adds parameter to search Item and Collection properties. | *Pilot* | | [Context](item-search/README.md#context) | [Item Search](item-search/) response ([ItemCollection](fragments/itemcollection/README.md)) | Adds search related metadata (context) to ItemCollection. | *Proposal* | | [Sort](item-search/README.md#sort) | [Item Search](item-search/) request | Adds Parameter to control sorting of returns results. | *Pilot* | | [Transaction](ogcapi-features/extensions/transaction/README.md) | [STAC - Features API](ogcapi-features) POST on `/items` endpoint, DELETE/PUT on `/items/{itemId}` endpoint | Adds PUT and DELETE endpoints for the creation, editing, and deleting of Item objects. | *Pilot* | | [Items and Collections API Version](ogcapi-features/extensions/version/README.md) | [STAC - Features API](ogcapi-features) on `/items` endpoint | Adds GET versions resource to Collection and Item endpoints and provides semantics for a versioning scheme for Collection and Item objects. | *Proposal* | +| [Query](item-search/README.md#query) | [Item Search](item-search/) request | Adds parameter to search Item and Collection properties. | *Deprecated* | ### Conformance classes of extensions Each extension has its own conformance URI, which is used in the `conformsTo` response of the landing page to let clients know what capabilities the service supports. This are listed at the top of each extension description, but the full table is given here for ease of reference. -| Extension Name | Conformance URI | -|-----------------------------------------------------------------------------------|----------------------------------------------------------------------------| -| [Fields](item-search/README.md#fields) | | -| [Query](item-search/README.md#query) | | -| [Context](item-search/README.md#context) | | -| [Sort](item-search/README.md#sort) | | -| [Transaction](ogcapi-features/extensions/transaction/README.md) | | -| [Items and Collections API Version](ogcapi-features/extensions/version/README.md) | | +| Extension Name | Conformance Class URIs | +|---------------|-------------------------| +| [Fields](item-search/README.md#fields) | | +| [Filter](item-search/README.md#filter) |



| +| [Context](item-search/README.md#context) | | +| [Sort](item-search/README.md#sort) | | +| [Transaction](ogcapi-features/extensions/transaction/README.md) | | +| [Items and Collections API Version](ogcapi-features/extensions/version/README.md) | | +| [Query](item-search/README.md#query) | | + ## Third-party / vendor extensions diff --git a/fragments/context/README.md b/fragments/context/README.md index 22c9cf6a..189617e4 100644 --- a/fragments/context/README.md +++ b/fragments/context/README.md @@ -1,6 +1,7 @@ # STAC API - Context Fragment - **OpenAPI specification:** [openapi.yaml](openapi.yaml) +- **Conformance Class:** - **Fragment [Maturity Classification](../../extensions.md#extension-maturity):** Pilot - **Dependents:** - [Item Search](../../item-search) diff --git a/fragments/fields/README.md b/fragments/fields/README.md index a3d54ccc..52087fa1 100644 --- a/fragments/fields/README.md +++ b/fragments/fields/README.md @@ -1,6 +1,7 @@ # STAC API - Fields Fragment - **OpenAPI specification:** [openapi.yaml](openapi.yaml) +- **Conformance Class:** - **Fragment [Maturity Classification](../../extensions.md#extension-maturity):** Pilot - **Dependents:** - [Item Search](../../item-search) diff --git a/fragments/filter/README.md b/fragments/filter/README.md new file mode 100644 index 00000000..21c9112e --- /dev/null +++ b/fragments/filter/README.md @@ -0,0 +1,794 @@ +# STAC API - Filter Fragment + +- **OpenAPI specification:** [openapi.yaml](openapi.yaml) +- **Conformance Classes:** + - Filter: + - Simple CQL: + - Item Search Filter: + - CQL Text: + - CQL JSON: + - Enhanced Spatial Operators: + - Enhanced Temporal Operators: + - Functions: + - Arithmetic: + - Arrays: +- **Extension [Maturity Classification](../../extensions.md#extension-maturity):** Pilot +- **Dependents:** + - [Item Search](../../item-search) + +- [STAC API - Filter Fragment](#stac-api---filter-fragment) + - [Overview](#overview) + - [Limitations of Item Search](#limitations-of-item-search) + - [Filter expressiveness](#filter-expressiveness) + - [Conformance Classes](#conformance-classes) + - [Getting Started with Implementation](#getting-started-with-implementation) + - [Queryables](#queryables) + - [GET Query Parameters and POST JSON fields](#get-query-parameters-and-post-json-fields) + - [Interaction with Endpoints](#interaction-with-endpoints) + - [Examples](#examples) + - [Example 1](#example-1) + - [Example 1: GET with cql-text](#example-1-get-with-cql-text) + - [Example 1: POST with cql-json](#example-1-post-with-cql-json) + - [Example 2](#example-2) + - [Example 2: GET with cql-text](#example-2-get-with-cql-text) + - [Example 2: POST with cql-json](#example-2-post-with-cql-json) + - [Example 3](#example-3) + - [Example 3: GET with cql-text](#example-3-get-with-cql-text) + - [Example 3: POST with cql-json](#example-3-post-with-cql-json) + - [Example 4](#example-4) + - [Example 4: AND cql-text (GET)](#example-4-and-cql-text-get) + - [Example 4: AND cql-json (POST)](#example-4-and-cql-json-post) + - [Example 5](#example-5) + - [Example 5: OR cql-text (GET)](#example-5-or-cql-text-get) + - [Example 5: OR cql-json (POST)](#example-5-or-cql-json-post) + - [Example 6: Temporal](#example-6-temporal) + - [Example 6: ANYINTERACTS cql-text (GET)](#example-6-anyinteracts-cql-text-get) + - [Example 6: ANYINTERACTS cql-json (POST)](#example-6-anyinteracts-cql-json-post) + - [Example 6: Spatial](#example-6-spatial) + - [Example 6: INTERSECTS cql-text (GET)](#example-6-intersects-cql-text-get) + - [Example 6: INTERSECTS cql-json (POST)](#example-6-intersects-cql-json-post) + +## Overview + +The Filter extension provides an expressive mechanism for searching based on Item attributes. + +This extension references behavior defined in the +[OGC API - Features - Part 3: Filtering and the Common Query Language (CQL)](https://portal.ogc.org/files/96288) +specification. As of May 2020, this specification is in draft status. The only anticipated change before final is to the +division of behavior among conformance classes, as described further +in the [Conformance Classes](#conformance-classes) section. + +OAFeat Part 3 CQL formally defines syntax for both a text format (cql-text) as an ABNF grammar (largely similar to the BNF grammar +in the General Model) and a JSON format (cql-json) as a JSON Schema and OpenAPI schema, and provides a precise natural +language description of the declarative semantics. The CQL Text format has long-standing use within +geospatial software (e.g., GeoServer), is expected not to change before final. +OGC CQL Text has been previously described in [OGC Filter Encoding](https://www.ogc.org/standards/filter) and +[OGC Catalogue Services 3.0 - General Model](http://docs.opengeospatial.org/is/12-168r6/12-168r6.html#62) +(including a BNF grammar in Annex B). The CQL JSON format is newly-defined, but also not +expected to change before final. + +It should be noted that the "CQL" referred to here is OGC CQL. It is **not** referencing or related two other "CQL" languages, +the [SRU (Search/Retrieve via URL) Contextual Query Language](https://www.loc.gov/standards/sru/cql/index.html) (formerly +known as Common Query Language) or the [Cassandra Query Language](https://cassandra.apache.org/doc/latest/cql/) used by the +Cassandra database. + +## Limitations of Item Search + +OAFeat defines a limited set of filtering capabilities. Filtering can only be done over a single collection and +with only a single `bbox` (rectangular spatial filter) parameter and a single datetime (instant or interval) parameter. + +The STAC Item Search specification extends the functionality of OAFeat in a few key ways: +- It allows cross-collection filtering, whereas OAFeat only allows filtering within a single collection. + (`collections` parameter, accepting 0 or more collections) +- It allows filtering by Item ID (`ids` parameter) +- It allows filtering based on a single GeoJSON Geometry, rather than only a bbox (`intersects` parameter) + +However, it does not contain a formalized way to filter based on arbitrary fields in an Item. For example, there is +no way to express the filter "item.properties.eo:cloud_cover is less than 10". It also does not have a way to logically combine +multiple spatial or temporal filters. + +## Filter expressiveness + +This extension expands the capabilities of Item Search and the OAFeat Items resource with +[OAFeat Part 3 CQL](https://portal.ogc.org/files/96288) +by providing an expressive query language to construct more complex filter predicates. The operators are similar to +those provided by SQL. The Simple CQL conformance class requires the logical operators `AND`, `OR`, and `NOT`; +the comparison operators `=`, `<`, `<=`, `>`, `>=`, `LIKE`, `IS NULL`, `BETWEEN`, and `IN`; the spatial operator +`INTERSECTS`; and the temporal operator `ANYINTERACTS`. + +The ANYINTERACTS operator has effectively the same semantics as the `datetime` parameter +in Item Search. + +CQL enables these types of queries: +- Use of Item Property values in predicates (e.g., `item.properties.eo:cloud_cover`), using comparison operators +- Items whose `datetime` values are in the month of August of the years 2017-2021, using OR and ANYINTERACTS +- Items whose `geometry` values intersect any one of several Polygons, using OR and INTERSECTS +- Items whose `geometry` values intersect one Polygon, but do not intersect another one, using AND, NOT, and + INTERSECTS + +This extension also supports the Queryables mechanism that allows discovery of what Item fields can be used in +predicates. + +## Conformance Classes + +OAFeat CQL defines several conformance classes that allow implementers to create compositions of +functionality that support whatever expressiveness they need. This allows implementers to incrementally support CQL +syntax, without needing to implement a huge spec all at once. Some implementers choose not to incur the cost of +implementing functionality they do not need or may not be able to implement functionality that is not supported by +their underlying datastore, e.g., Elasticsearch does not support the spatial predicates required by the +Enhanced Spatial Operators conformance class. + +The STAC API Filter Extension reuses the definitions of several conformance classes defined in OAFeat CQL, but with a prefix of +`https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:` instead of `http://www.opengis.net/spec/ogcapi-features-3/1.0/req/`. + +The implementation must support these conformance classes: + +- Filter (`https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:filter`) defines the Queryables mechanism and + parameters `filter-lang`, `filter-crs`, and `filter`. +- Simple CQL (`https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:simple-cql`) defines the query language used + for the `filter` parameter defined by Filter. +- Item Search Filter (`https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:item-search-filter`) binds the Filter and Simple CQL + conformance classes to apply to the Item Search endpoint (`/search`). This class is the correlate of the OAFeat CQL Features + Filter class that binds Filter and Simple CQL to the Features resource (`/collections/{cid}/items`). + +The implementation must support at least one of the "CQL Text" or "CQL JSON" conformance classes that define the CQL format +used in the filter parameter: + +- CQL Text (`https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:cql-text`) defines that the CQL Text format is supported by Item Search. +- CQL JSON (`https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:cql-json`) defines that the CQL JSON format is supported by Item Search + +If both are advertised as being supported, it is only +required that both be supported for GET query parameters, and that only that CQL JSON be supported for POST JSON requests. +It is recommended that clients use CQL Text in GET requests and CQL JSON in POST requests. + +The Filter Extension defines support for the Enhanced Spatial Operators, Enhanced Temporal Operators, +Functions, Arithmetic Expressions, or Arrays conformance classes defined in OAFeat CQL. Implementing these conformance +classes and their operations is encouraged but not required. Implementation of these is often limited by the +operations supported by the implementation's datastore, for example, Elasticsearch does not support the spatial +operations required by the Enhanced Spatial Operators. If implemented for Item Search, the conformance class URI should follow +the same pattern relative to OAFeat CQL. + +Additionally, if an API implements the OGC API Features endpoint, it is **recommended** that the OAFeat Part 3 Filter, +Features Filter, and Simple CQL conformance classes be implemented, which allow use of CQL filters against the +OAFeat Part 1 Features endpoint (`/collections/{collectionId}/items`). Note that POST with a JSON body +to the Features resource is not supported, as POST is used by the +[Transaction Extension](../../ogcapi-features/extensions/transaction/README.md) for creating items. + +It is likely that the OAFeat "Simple CQL" conformance class will be decomposed into several smaller conformance classes, +as described [here](https://github.com/opengeospatial/ogcapi-features/issues/579). Until the OAFeat CQL reaches final, it is +considered compliant within a STAC API to advertise the Simple CQL conformance class but only partially-implement it, e.g., to +only implement the logical and comparison operators. It is recommended +to provide an out-of-band way for users to discover what operators from the Simple CQL class are implemented. After OAFeat CQL is +finalized, implementations will be expected to provide correct conformance classes with respect to their implementation of CQL. + +An additional change that may be made to the Simple CQL conformance class is to only require support of expressions with a property +name of the left hand side and a literal on the right hand side (e.g., `eo:cloud_cover <= 10`), and additional conformance classes to +will support arbitrary uses of properties and literals in expressions on either side. The primary motivation for this is to allow +implementations that use datastores that do not easily support right-hand side properties to implement Simple CQL +(e.g., Elasticsearch). Implementers should feel free to only implement `property operand literal` expressions at this time. + +## Getting Started with Implementation + +It recommended that implementers start with fully implementing only a subset of functionality. As stated previously, +until the OAFeat CQL spec is finalized, it is legal in a STAC API implementation to advertise that the Simple CQL conformance +class is implemented, but that only a subset of that functionality is implemented. + +A good place to start is +implementing only the logical and simple comparison operators (`=`, `<`, `<=`, `>`, `>=`), defining a static Queryables +schema with no queryables advertised, and only implementing CQL Text. Following from that can be support for +INTERSECTS and ANYINTERACTS, defining a static Queryables schema with only the basic Item properties, and also +implementing CQL JSON. From there, other comparison operators can be implemented and a more +dynamic Queryables schema (if desired). + +Formal definitions and grammars for CQL can be found here: + +- The [OAFeat (CQL) spec](https://portal.ogc.org/files/96288) includes an ABNF for cql-text and both JSON Schema and + OpenAPI specifications for cql-json. The standalone files are: + - [cql.bnf](https://github.com/opengeospatial/ogcapi-features/blob/master/extensions/cql/standard/schema/cql.bnf) + - [cql.json](https://github.com/opengeospatial/ogcapi-features/blob/master/extensions/cql/standard/schema/cql.json) + - [cql.yml](https://github.com/opengeospatial/ogcapi-features/blob/master/extensions/cql/standard/schema/cql.yml) +- A JSON Schema for only the parts of the CQL JSON encoding required by this extension is [here](cql.json) +- A OpenAPI specification for only the parts of the CQL JSON encoding required by this extension is [here](cql.yml) +- xtraplatform-spatial has a CQL [ANTLR 4 grammer](https://github.com/interactive-instruments/xtraplatform-spatial/tree/master/xtraplatform-cql/src/main/antlr/de/ii/xtraplatform/cql/infra) + +These projects have or are developing CQL support: + +- [GeoPython PyCQL](https://github.com/geopython/pycql/tree/master/pycql), and the + [Bitner fork](https://github.com/bitner/pycql) to be used in stac-fastapi +- [pygeofilter](https://github.com/geopython/pygeofilter) has support for ECQL (a superset of CQL Text) and CQL JSON +- [Franklin](https://github.com/azavea/franklin) is working on it in [this PR](https://github.com/azavea/franklin/pull/750). +- [Geotools](https://github.com/geotools/geotools) has support for [CQL text](https://github.com/geotools/geotools/tree/main/modules/library/cql/src/main/java/org/geotools/filter/text/cql2) + +Note that the [xbib CQL library (JVM)](https://github.com/xbib/cql) is the OASIS Contextual Query Language, not +OGC CQL, and should not be used to implement this extension, as they are significantly different query languages. +[Stacatto](https://github.com/planetlabs/staccato) uses this for their query language implementation, but it is +not compliant with this extension. + +## Queryables + +The Queryables mechanism allows a client to discover what variable terms are available for use when writing filter +expressions. These variables can be defined per-collection, and the intersection of these variables over all collections is what +is available for filtering when there are no collection restrictions. These queryables are the only variables that may be used +in filter expressions, and if any variable is used in expression that is not defined as a queryable and error should be +returned according to OAFeat Part 3. It is recognized that this is a severe restriction in STAC APIs that have highly variable +and dynamic content. It is possible that this will change in the OAFeat Part 3 spec, see +this [issue](https://github.com/opengeospatial/ogcapi-features/issues/582). For now, implementers may choose to allow +fully-qualified property +names not advertised in queryables (e.g., `properties.eo:cloud_cover`, in anticipation that there be some +mechanism to explicitly allow or advertise this in the future. + +Queryables are advertised via a JSON Schema document retrieved from the `/queryables` endpoint. This endpoint at the root +retrieves queryables that apply to all collections. When used as a subresource of the collection resource +(e.g. /collections/collection1/queryables), it returns queryables pertaining only to that single collection. + +It is required to implement both of these endpoints, but for a STAC API, this may simply be a static document of the +STAC-specific fields. A basic Queryables +definitions for STAC Items should include at least the fields id, collection, geometry, and datetime. + +```json +{ + "$schema" : "https://json-schema.org/draft/2019-09/schema", + "$id" : "https://example.org/queryables", + "type" : "object", + "title" : "Queryables for Example STAC API", + "description" : "Queryable names for the example STAC API Item Search filter.", + "properties" : { + "id" : { + "description" : "ID", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id" + }, + "collection" : { + "description" : "Collection", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection" + }, + "geometry" : { + "description" : "Geometry", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry" + }, + "datetime" : { + "description" : "Datetime", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime" + } + } +} +``` + +Fields in Item Properties should be exposed with their un-prefixed names, and not require expressions to prefix them +with `properties`, e.g., `eo:cloud_cover` instead of `properties.eo:cloud_cover`. + +There may also be support for finding what queryables are available for a subset of collections, e.g., +`/queryables?collections=c1,c3`) as described in [this issue](https://github.com/opengeospatial/ogcapi-features/issues/576). + +The Landing Page endpoint (`/`) will have a Link with rel `http://www.opengis.net/def/rel/ogc/1.0/queryables` with an href to +the endpoint `/queryables`. Additionally, each Collection resource will have a Link to the queryables resource for that +collection, e.g., `/collections/collection1/queryables`. + +The queryables endpoint returns a JSON Schema describing the names and types variables that may be used in filter expressions. +This response is defined in JSON Schema because it is a well-specified typed schema, but it is not used for validating a JSON +document derived from it. This schema defines the variables that may be used in a CQL filter. + +These queryable variables are mapped by the service to filter Items. For example, the service may define a queryable with the +name "eo:cloud_cover" that can be used in a CQL expression like `eo:cloud_cover <= 10`, with the semantics that only Items where the +`properties.eo:cloud_cover` value was <= 10 would match the filter. The server would then translate this into an appropriate +query for the data within its datastore. + +Queryables can be static or dynamically derived. For example, `cloud_cover` might be specified to have a value 0 to 100 or a field +may have a set of enumerated values dynamically determined by an aggreation at runtime. This schema can be used by a UI or +interactive client to dynamically expose to the user the fields that are available for filtering, and provide a precise group +of options for the values of these variables. + +Queryables can also be used to advertise synthesized property values. The only requirement in CQL is that the property have a type +and evaluate to literal value of that type or NULL. For example, a filter like "Items must have an Asset with an eo:band with +the common_name of 'nir'" can be expressed. A Queryable `assets_bands` could be defined to have a type of array of string and +have the semantics that it contains all of `common_name` values across all assets and bands for an Item. This could then be +filtered with the CQL expression `'nir' in assets_bands`. Implementations would then expand this expression into the +appropriate query against its datastore. (TBD if this will actually work or not. This is also related to the upcoming +restriction on property/literal comparisons) + +An implementation may also choose not to advertise any queryables, and provide the user with out-of-band information or +simply let them try querying against fields. While this is not allowed according to the OGC CQL Queryable spec, it is allowed +in STAC API by the Filter Extension. In this case, the queryables endpoint (`/queryables`) would return this document: + +```json +{ + "$schema" : "https://json-schema.org/draft/2019-09/schema", + "$id" : "https://example.org/queryables", + "type" : "object", + "title" : "Queryables for Example STAC API", + "description" : "Queryable names for the example STAC API Item Search filter.", + "properties" : { + } +} +``` + +## GET Query Parameters and POST JSON fields + +This extension adds three GET query parameters or POST JSON fields to an Item Search request: + +- filter-lang:`cql-text` or `cql-json`. If undefined, defaults to `cql-text` for a GET request and `cql-json` for a POST request. +- filter-crs: recommended to not be passed, but server must only accept `http://www.opengis.net/def/crs/OGC/1.3/CRS84` as + a valid value, may reject any others +- filter: CQL filter expression + +API implementations advertise which `filter-lang` values are supported via conformance classes in the Landing Page. +At least one must be implemented, but it is recommended to implement both. If both are advertised as conformance classes, the +server should process either for a GET request, but may only process cql-json for a POST request. If POST of cql-text is not +supported, the server must return a 400 error if `filter-lang=cql-text`. + +## Interaction with Endpoints + +In an implementation that supports Simple CQL, the Landing Page (`/`) should return a document including at least these values: + +```json +{ + "id": "example_stacapi", + "conformsTo": [ + "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core", + "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/oas30", + "http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/geojson", + + "http://www.opengis.net/spec/ogcapi_common-2/1.0/req/collections", + + "https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:filter", + "https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:features-filter", + "https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:simple-cql", + "https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:cql-text", + "https://api.stacspec.org/v1.0.0-beta.1/item-search#filter:cql-json", + + "http://stacspec.org/spec/api/1.0.0-beta.1/core", + "http://stacspec.org/spec/api/1.0.0-beta.1/req/stac-search", + "http://stacspec.org/spec/api/1.0.0-beta.1/req/stac-response" + ], + "links": [ + { + "title": "Search", + "href": "https://example.org/search", + "rel": "search", + "type": "application/geo+json" + }, + { + "title": "Queryables", + "href": "https://example.org/queryables", + "rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables", + "type": "application/schema+json" + } + ], + "stac_extensions": [], + "stac_version": "1.0.0", +} +``` + +A client can use the link with `"rel": "http://www.opengis.net/def/rel/ogc/1.0/queryables"` to retrieve the queryables. + +The Queryables endpoint (`/queryables`) returns something like the following: + +```json +{ + "$schema" : "https://json-schema.org/draft/2019-09/schema", + "$id" : "https://example.org/queryables", + "type" : "object", + "title" : "Queryables for Example STAC API", + "description" : "Queryable names for the example STAC API Item Search filter.", + "properties" : { + "id" : { + "description" : "ID", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/id" + }, + "collection" : { + "description" : "Collection", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/collection" + }, + "geometry" : { + "description" : "Geometry", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry" + }, + "datetime" : { + "description" : "Datetime", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime" + }, + "eo:cloud_cover" : { + "description" : "Cloud Cover", + "$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/properties/eo:cloud_cover" + }, + "gsd" : { + "description" : "Ground Sample Distance", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/instrument.json#/properties/gsd" + }, + "assets_bands" : { + "description" : "Asset eo:bands common names", + "$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/properties/eo:bands/common_name" + } + } +} +``` + +Alternately, the client could retrieve the queryables for a single collection with +`/collections/collections1/queryables`, which may have more queryables than available for the entire catalog, since +there may be queryables that are only relevant to that collection. + +Notice in this schema that instead of directly defining the type information about each field, we have instead used the JSON +Schema `$ref` mechanism to refer to a STAC schema definition. This not only allows the reuse of these JSON Schema definitions, +but also binds an arbitrarily-named Queryable to a specific STAC field. For example, in the above we know that the +`eo:cloud_cover` field is referring to the field of the same name in the EO Extension not because they happen to have the same +name, but rather because the `$ref` indicates it. The field could just as well be named "cloud_cover", "CloudCover", or "cc", +and we would still know it filtered on the EO extension `eo:cloud_cover` field. For example, if the queryable was named +"CloudCover", a CQL expression using that queryable would look like `CloudCover <= 10`. + +While these do seem quite complex to write and understand, keep in mind that query construction will likely be done with a +more ergonomic SDK, and query parsing will be done with the help of a ABNF grammar and OpenAPI schema. + +These parameters/fields must be supported by the STAC Item Search endpoint and OAFeat Features resource (`/collections/$collectionId/items`). + +## Examples + +Note: the GET examples with query parameters are unescaped to make them easier to read. + +The parameter `filter-crs` always defaults to `http://www.opengis.net/def/crs/OGC/1.3/CRS84` for a STAC API, so is not shown +in any of these examples. + +### Example 1 + +This example uses the queryables definition in (Interaction with Endpoints)(#interaction-with-endpoints). + +#### Example 1: GET with cql-text + +Note that `filter-lang` defaults to `cql-text` in this case. The parameter `filter-crs` defaults +to `http://www.opengis.net/def/crs/OGC/1.3/CRS84` for a STAC API. + +```javascript +GET /search?filter=id='LC08_L1TP_060247_20180905_20180912_01_T1_L1TP' AND collection='landsat8_l1tp' +``` + +#### Example 1: POST with cql-json + +Note that `filter-lang` defaults to `cql-json` in this case. The parameter `filter-crs` defaults +to `http://www.opengis.net/def/crs/OGC/1.3/CRS84` for a STAC API. + +```javascript +POST /search +{ + "filter": { + "and": [ + "eq": [ + { "property": "id" }, + "LC08_L1TP_060247_20180905_20180912_01_T1_L1TP" + ], + "eq": [ + { "property": "collection" }, + "landsat8_l1tp" + ] + ] + } +} +``` + +### Example 2 + +This example uses the queryables definition in [Interaction with Endpoints](#interaction-with-endpoints). + +Note that filtering on the `collection` field is relevant in Item Search, since the queries are cross-collection, whereas +OGC API Features filters only operate against a single collection already. + +#### Example 2: GET with cql-text + +```javascript +GET /search?filter=collection = 'landsat8_l1tp' + AND gsd <= 30 + AND eo:cloud_cover <= 10 + AND datetime ANYINTERACTS 2021-04-08T04:39:23Z/2021-05-07T12:27:57Z + AND INTERSECTS(geometry, POLYGON((43.5845 -79.5442, 43.6079 -79.4893, 43.5677 -79.4632, 43.6129 -79.3925, 43.6223 -79.3238, 43.6576 -79.3163, 43.7945 -79.1178, 43.8144 -79.1542, 43.8555 -79.1714, 43.7509 -79.6390, 43.5845 -79.5442)) +``` + +#### Example 2: POST with cql-json + +```javascript +POST /search +{ + "filter-lang": "cql-json", + "filter": { + "and": [ + "eq": [ + { "property": "collection" }, + "landsat8_l1tp" + ], + "lte": [ + { "property": "eo:cloud_cover" }, + "10" + ], + "anyinteracts": [ + { "property": "datetime" }, + [ "2021-04-08T04:39:23Z", "2021-05-07T12:27:57Z" ] + ], + "intersects": [ + { "property": "geometry" }, + { + "type": "Polygon", + "coordinates": [ + [ + [43.5845,-79.5442], + [43.6079,-79.4893], + [43.5677,-79.4632], + [43.6129,-79.3925], + [43.6223,-79.3238], + [43.6576,-79.3163], + [43.7945,-79.1178], + [43.8144,-79.1542], + [43.8555,-79.1714], + [43.7509,-79.6390], + [43.5845,-79.5442] + ] + ] + } + ] + ] + } +} +``` + +### Example 3 + +Queryable properties can be used on either side of an operator. This is a generic example, as there are are few STAC properties +that are comparable in a meaningful way. This example uses a contrived example of two proprietary properties, `prop1` and `prop2` that are of the +same type. + +This queryables JSON Schema is used in these examples: + +```json +{ + "$schema" : "https://json-schema.org/draft/2019-09/schema", + "$id" : "https://example.org/queryables", + "type" : "object", + "title" : "Queryables for Example STAC API", + "description" : "Queryable names for the example STAC API Item Search filter.", + "properties" : { + "prop1" : { + "description" : "Property 1", + "type": "integer" + }, + "prop2" : { + "description" : "Property 2", + "type": "integer" + } + } +} +``` + +#### Example 3: GET with cql-text + +```javascript +GET /search?filter=prop1 = prop2 +``` + +#### Example 3: POST with cql-json + +```javascript +POST /search +{ + "filter-lang": "cql-json", + "filter": { + "eq": [ + { "property": "prop1" }, + { "property": "prop2" } + ] + } +} +``` + +### Example 4 + +We'll be imagining these as queries against [EarthSearch Sentinel 2 +COG](https://stacindex.org/catalogs/earth-search#/Cnz1sryATwWudkxyZekxWx6356v9RmvvCcLLw79uHWJUDvt2?t=items) data. + +The queryables defined are as follows: + +```json +{ + "$schema" : "https://json-schema.org/draft/2019-09/schema", + "$id" : "https://example.org/queryables", + "type" : "object", + "title" : "Queryables for Example STAC API", + "description" : "Queryable names for the example STAC API Item Search filter.", + "properties" : { + "geometry" : { + "description" : "Geometry", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/item.json#/geometry" + }, + "datetime" : { + "description" : "Datetime", + "$ref": "https://schemas.stacspec.org/v1.0.0/item-spec/json-schema/datetime.json#/properties/datetime" + }, + "eo:cloud_cover" : { + "description" : "Cloud Cover", + "$ref": "https://stac-extensions.github.io/eo/v1.0.0/schema.json#/properties/eo:cloud_cover" + }, + "acme:data_coverage" : { + "description" : "Acme Sat Data Coverage", + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "acme:grid_id" : { + "description" : "Acme Sat Grid ID", + "type": "string" + } + } +} +``` + +Note that `acme:data_coverage` and `acme:grid_id` are properties that are not defined in an extension schema, and are intended to +represent vendor-specific properties. Because of this, they are fully specified directly in the JSON Schema. However, it is +recommended that vendor-specific properties be published as part of a well-defined extension schema, so these only represent ones +that have not followed that recommendation. + +A sample STAC Item (excluding `assets`) is: + +```json +{ + "type": "Feature", + "stac_version": "1.0.0-beta.2", + "stac_extensions": [ + "eo", + "view", + "proj" + ], + "id": "S2A_60HWB_20201111_0_L2A", + "bbox": [ 176.9997779369264, -39.83783732066656, 178.14624317719924, -38.842842449352425], + "geometry": { + "type": "Polygon", + "coordinates": [[[176.9997779369264, -39.83783732066656],[176.99978104582647,-38.84846679951431], + [178.14624317719924, -38.842842449352425],[177.8514661209684,-39.83471270154608], + [176.9997779369264, -39.83783732066656]]] + }, + "properties": { + "datetime": "2020-11-11T22:16:58Z", + "platform": "sentinel-2a", + "constellation": "sentinel-2", + "instruments": ["msi"], + "gsd": 10, + "view:off_nadir": 0, + "proj:epsg": 32760, + "sentinel:utm_zone": 60, + "sentinel:latitude_band": "H", + "sentinel:grid_square": "WB", + "sentinel:sequence": "0", + "sentinel:product_id": "S2A_MSIL2A_20201111T221611_N0214_R129_T60HWB_20201111T235959", + "sentinel:data_coverage": 78.49, + "eo:cloud_cover": 0.85, + "sentinel:valid_cloud_cover": true, + "created": "2020-11-12T02:08:31.563Z", + "updated": "2020-11-12T02:08:31.563Z" + } +} +``` + +One problem in working with Sentinel-2 data is that many scenes only contain a tiny "sliver" of data, where the satellite's +recording path intersection only a corner of a grid square. This examples shows +Show me all imagery that has low cloud cover (less than 10), and high data coverage (50), as I'd like a cloud free image that is not just +a tiny sliver of data. + +#### Example 4: AND cql-text (GET) + +```javascript +/search?filter=sentinel:data_coverage > 50 AND eo:cloud_cover < 10 +``` + +#### Example 4: AND cql-json (POST) + +```json +{ + "filter": { + "and": [ + { + "gt": [ + { + "property": "sentinel:data_coverage" + }, + 50 + ] + }, + { + "lt": [ + { + "property": "eo:cloud_cover" + }, + 10 + ] + } + ] + } +} +``` + +An 'or' is also supported, matching if either condition is true. Though it's not a sensible query you could get images that have full data +coverage or low cloud cover. + +### Example 5 + +This uses the same queryables as Example 4. + +#### Example 5: OR cql-text (GET) + +```javascript +/search?filter=sentinel:data_coverage > 50 OR eo:cloud_cover < 10 +``` + +#### Example 5: OR cql-json (POST) + +```json +{ + "filter": { + "or": [ + { + "gt": [ + { "property": "sentinel:data_coverage" }, + 50 + ] + }, + { + "lt": [ + { "property": "eo:cloud_cover" }, + 10 + ] + } + ] + } +} +``` + +### Example 6: Temporal + +This uses the same queryables as Example 4. + +The only temporal operator required is `ANYINTERACTS`, which follows the same semantics as the existing +`datetime` parameter. This is effectively that the datetime or interval operands have any overlap between them. + +#### Example 6: ANYINTERACTS cql-text (GET) + +```javascript +/search?filter=datetime ANYINTERACTS 2020-11-11 +``` + +#### Example 6: ANYINTERACTS cql-json (POST) + +```json +{ + "filter": { + "anyinteracts": [ + { "property": "datetime" }, + "2020-11-11" + ] + } +} +``` + +### Example 6: Spatial + +The only spatial operator that must be implemented is `INTERSECTS`. This has the same semantics as the one provided +in the Item Search `intersects` parameter. The `cql-text` format uses WKT geometries and the `cql-json` format uses +GeoJSON geometries. + +#### Example 6: INTERSECTS cql-text (GET) + +```javascript +/search?filter=INTERSECTS(geometry,POLYGON((-77.0824 38.7886,-77.0189 38.7886,-77.0189 38.8351,-77.0824 38.8351,-77.0824 38.7886))) +``` + +#### Example 6: INTERSECTS cql-json (POST) + +```json +{ + "filter": { + "intersects": [ + { "property": "geometry" } , + { + "type": "Polygon", + "coordinates": [[ + [-77.0824, 38.7886], [-77.0189, 38.7886], + [-77.0189, 38.8351], [-77.0824, 38.8351], + [-77.0824, 38.7886] + ]] + } + ] + }, +} +``` diff --git a/fragments/filter/cql.json b/fragments/filter/cql.json new file mode 100644 index 00000000..0bfbef33 --- /dev/null +++ b/fragments/filter/cql.json @@ -0,0 +1,557 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "type": "object", + "oneOf": [ + {"$ref": "#/$defs/andExpression"}, + {"$ref": "#/$defs/orExpression"}, + {"$ref": "#/$defs/notExpression"}, + {"$ref": "#/$defs/comparisonPredicate"}, + {"$ref": "#/$defs/spatialPredicate"}, + {"$ref": "#/$defs/temporalPredicate"} + ], + "$recursiveAnchor": true, + "$defs": { + "andExpression": { + "type": "object", + "required": ["and"], + "properties": { + "and": { + "type": "array", + "minItems": 2, + "items": { + "$recursiveRef": "#" + } + } + } + }, + + "orExpression": { + "type": "object", + "required": ["or"], + "properties": { + "or": { + "type": "array", + "minItems": 2, + "items": { + "$recursiveRef": "#" + } + } + } + }, + + "notExpression": { + "type": "object", + "required": ["not"], + "properties": { + "not": { + "type": "array", + "minItems": 1, + "maxItems": 1, + "items": { + "$recursiveRef": "#" + } + } + } + }, + + "comparisonPredicate" : { + "oneOf": [ + {"$ref": "#/$defs/binaryComparisonPredicate"}, + {"$ref": "#/$defs/isLikePredicate" }, + {"$ref": "#/$defs/isBetweenPredicate"}, + {"$ref": "#/$defs/isInListPredicate" }, + {"$ref": "#/$defs/isNullPredicate" } + ] + }, + + "binaryComparisonPredicate": { + "oneOf": [ + { "$ref": "#/$defs/eqExpression" }, + { "$ref": "#/$defs/ltExpression" }, + { "$ref": "#/$defs/gtExpression" }, + { "$ref": "#/$defs/lteExpression" }, + { "$ref": "#/$defs/gteExpression" } + ] + }, + + "eqExpression": { + "type": "object", + "required": ["eq"], + "properties": { + "eq": { "$ref": "#/$defs/scalarOperands" } + } + }, + + "ltExpression": { + "type": "object", + "required": ["lt"], + "properties": { + "lt": { "$ref": "#/$defs/scalarOperands" } + } + }, + + "gtExpression": { + "type": "object", + "required": ["gt"], + "properties": { + "gt": { "$ref": "#/$defs/scalarOperands" } + } + }, + + "lteExpression": { + "type": "object", + "required": ["lte"], + "properties": { + "lte": { "$ref": "#/$defs/scalarOperands" } + } + }, + + "gteExpression": { + "type": "object", + "required": ["gte"], + "properties": { + "gte": { "$ref": "#/$defs/scalarOperands" } + } + }, + + "isBetweenPredicate": { + "type": "object", + "required": ["between"], + "properties": { + "between": { + "type": "object", + "required" : [ "value", "lower", "upper" ], + "properties": { + "value": { "$ref": "#/$defs/valueExpression" }, + "lower": { "$ref": "#/$defs/scalarExpression" }, + "upper": { "$ref": "#/$defs/scalarExpression" } + } + } + } + }, + + "isLikePredicate": { + "type": "object", + "required": ["like"], + "properties": { + "like": { "$ref": "#/$defs/scalarOperands" }, + "wildcard": { "type": "string", "default": "%" }, + "singleChar": { "type": "string", "default": "." }, + "escapeChar": { "type": "string", "default": "\\" }, + "nocase": { "type": "boolean", "default": true } + } + }, + + "isInListPredicate": { + "type": "object", + "required": ["in"], + "properties": { + "in": { + "type": "object", + "required": ["value","list"], + "properties": { + "value": { "$ref": "#/$defs/valueExpression" }, + "list": { + "type": "array", + "items": { + "$ref": "#/$defs/valueExpression" + } + }, + "nocase": { + "type": "boolean", + "default": true + } + } + } + } + }, + + "valueExpression": { + "oneOf": [ + {"$ref": "#/$defs/scalarExpression"}, + {"$ref": "#/$defs/spatialLiteral"}, + {"$ref": "#/$defs/typedTemporalLiteral"} + ] + }, + + "scalarOperands": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "$ref": "#/$defs/scalarExpression" + } + }, + + "scalarExpression": { + "oneOf": [ + {"$ref": "#/$defs/scalarLiteral"}, + {"$ref": "#/$defs/propertyRef"} + ] + }, + + "isNullPredicate": { + "type": "object", + "required": [ "isNull" ], + "properties": { + "isNull": { + "$ref": "#/$defs/scalarExpression" + } + } + }, + + "spatialPredicate" : { + "oneOf": [ + {"$ref": "#/$defs/intersectsExpression"} + ] + }, + + "intersectsExpression": { + "type": "object", + "required": ["intersects"], + "properties": { + "intersects": { "$ref": "#/$defs/spatialOperands" } + } + }, + + "spatialOperands": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "$ref": "#/$defs/geomExpression" + } + }, + + "geomExpression": { + "oneOf": [ + {"$ref": "#/$defs/spatialLiteral"}, + {"$ref": "#/$defs/propertyRef"} + ] + }, + + "temporalPredicate" : { + "oneOf": [ + {"$ref": "#/$defs/anyinteractsExpression"} + ] + }, + + "anyinteractsExpression": { + "type": "object", + "required": ["anyinteracts"], + "properties": { + "anyinteracts": { "$ref": "#/$defs/temporalOperands" } + } + }, + + "temporalOperands": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "$ref": "#/$defs/temporalExpression" + } + }, + + "temporalExpression": { + "oneOf": [ + {"$ref": "#/$defs/temporalLiteral"}, + {"$ref": "#/$defs/propertyRef"} + ] + }, + + "propertyRef": { + "type": "object", + "required": ["property"], + "properties": { + "propertyName": { "type": "string" } + } + }, + + "scalarLiteral": { + "oneOf": [ + { "type": "string" }, + { "type": "number" }, + { "type": "boolean"} + ] + }, + + "spatialLiteral": { + "oneOf": [ + { "$ref": "#/$defs/geometryLiteral" }, + { "$ref": "#/$defs/envelopeLiteral" } + ] + }, + + "geometryLiteral": { + "oneOf": [ + { "$ref": "#/$defs/point" }, + { "$ref": "#/$defs/linestring" }, + { "$ref": "#/$defs/polygon" }, + { "$ref": "#/$defs/multipoint" }, + { "$ref": "#/$defs/multilinestring" }, + { "$ref": "#/$defs/multipolygon" } + ] + }, + + "point": { + "title": "GeoJSON Point", + "type": "object", + "required": ["type","coordinates"], + "properties": { + "type": { + "type": "string", + "enum": ["Point"] + }, + "coordinates": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + + "linestring": { + "title": "GeoJSON LineString", + "type": "object", + "required": ["type","coordinates"], + "properties": { + "type": { + "type": "string", + "enum": ["LineString"] + }, + "coordinates": { + "type": "array", + "minItems": 2, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + + "polygon": { + "title": "GeoJSON Polygon", + "type": "object", + "required": ["type","coordinates"], + "properties": { + "type": { + "type": "string", + "enum": ["Polygon"] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + + "multipoint": { + "title": "GeoJSON MultiPoint", + "type": "object", + "required": ["type","coordinates"], + "properties": { + "type": { + "type": "string", + "enum": ["MultiPoint"] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + + "multilinestring": { + "title": "GeoJSON MultiLineString", + "type": "object", + "required": ["type","coordinates"], + "properties": { + "type": { + "type": "string", + "enum": ["MultiLineString"] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + + "multipolygon": { + "title": "GeoJSON MultiPolygon", + "type": "object", + "required": ["type","coordinates"], + "properties": { + "type": { + "type": "string", + "enum": ["MultiPolygon"] + }, + "coordinates": { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "array", + "minItems": 4, + "items": { + "type": "array", + "minItems": 2, + "items": { + "type": "number" + } + } + } + } + }, + "bbox": { + "type": "array", + "minItems": 4, + "items": { + "type": "number" + } + } + } + }, + + "envelopeLiteral": { + "type": "object", + "required": [ "bbox" ], + "properties": { + "bbox": { "$ref": "#/$defs/bbox" } + } + }, + + "bbox": { + "type": "array", + "oneOf": [ + { "minItems": 4, "maxItems": 4}, + { "minItems": 6, "maxItems": 6} + ], + "items": { + "type": "number" + } + }, + + "temporalLiteral": { + "oneOf": [ + { "$ref": "#/$defs/timeString" }, + { "$ref": "#/$defs/periodString" } + ] + }, + + "timeString": { + "oneOf": [ + { "type": "string", "format": "date" }, + { "type": "string", "format": "date-time" } + ] + }, + + "periodString": { + "type": "array", + "minItems": 2, + "maxItems": 2, + "items": { + "oneOf": [ + { "$ref": "#/$defs/timeString" }, + { "type": "string", "enum": [".."] } + ] + } + }, + + "typedTemporalLiteral": { + "oneOf": [ + { "$ref": "#/$defs/typedTimeString" }, + { "$ref": "#/$defs/typedPeriodString" } + ] + }, + + "typedTimeString": { + "type": "object", + "required": ["datetime"], + "properties": { + "datetime": { + "$ref": "#/$defs/timeString" + } + } + }, + + "typedPeriodString": { + "type": "object", + "required": ["datetime"], + "properties": { + "datetime": { + "$ref": "#/$defs/periodString" + } + } + } + } +} + diff --git a/fragments/filter/cql.yml b/fragments/filter/cql.yml new file mode 100644 index 00000000..41abdeab --- /dev/null +++ b/fragments/filter/cql.yml @@ -0,0 +1,434 @@ +--- +openapi: 3.0.3 +info: + title: Schema of Common Query Language (CQL) + description: 'For use in OpenAPI 3.0 documents' + version: '1.0.0-draft.1' +paths: {} +components: + schemas: + booleanValueExpression: + type: object + oneOf: + - "$ref": "#/components/schemas/andExpression" + - "$ref": "#/components/schemas/orExpression" + - "$ref": "#/components/schemas/notExpression" + - "$ref": "#/components/schemas/comparisonPredicate" + - "$ref": "#/components/schemas/spatialPredicate" + - "$ref": "#/components/schemas/temporalPredicate" + andExpression: + type: object + required: + - and + properties: + and: + type: array + minItems: 2 + items: + "$ref": "#/components/schemas/booleanValueExpression" + orExpression: + type: object + required: + - or + properties: + or: + type: array + minItems: 2 + items: + "$ref": "#/components/schemas/booleanValueExpression" + notExpression: + type: object + required: + - not + properties: + not: + type: array + minItems: 1 + maxItems: 1 + items: + "$ref": "#/components/schemas/booleanValueExpression" + comparisonPredicate: + oneOf: + - "$ref": "#/components/schemas/binaryComparisonPredicate" + - "$ref": "#/components/schemas/isLikePredicate" + - "$ref": "#/components/schemas/isBetweenPredicate" + - "$ref": "#/components/schemas/isInListPredicate" + - "$ref": "#/components/schemas/isNullPredicate" + binaryComparisonPredicate: + oneOf: + - "$ref": "#/components/schemas/eqExpression" + - "$ref": "#/components/schemas/ltExpression" + - "$ref": "#/components/schemas/gtExpression" + - "$ref": "#/components/schemas/lteExpression" + - "$ref": "#/components/schemas/gteExpression" + eqExpression: + type: object + required: + - eq + properties: + eq: + "$ref": "#/components/schemas/scalarOperands" + ltExpression: + type: object + required: + - lt + properties: + lt: + "$ref": "#/components/schemas/scalarOperands" + gtExpression: + type: object + required: + - gt + properties: + gt: + "$ref": "#/components/schemas/scalarOperands" + lteExpression: + type: object + required: + - lte + properties: + lte: + "$ref": "#/components/schemas/scalarOperands" + gteExpression: + type: object + required: + - gte + properties: + gte: + "$ref": "#/components/schemas/scalarOperands" + isBetweenPredicate: + type: object + required: + - between + properties: + between: + type: object + required: + - value + - lower + - upper + properties: + value: + "$ref": "#/components/schemas/valueExpression" + lower: + "$ref": "#/components/schemas/scalarExpression" + upper: + "$ref": "#/components/schemas/scalarExpression" + isLikePredicate: + type: object + required: + - like + properties: + like: + "$ref": "#/components/schemas/scalarOperands" + wildcard: + type: string + default: "%" + singleChar: + type: string + default: "." + escapeChar: + type: string + default: "\\" + nocase: + type: boolean + default: true + isInListPredicate: + type: object + required: + - in + properties: + in: + type: object + required: + - value + - list + properties: + value: + "$ref": "#/components/schemas/valueExpression" + list: + type: array + items: + "$ref": "#/components/schemas/valueExpression" + nocase: + type: boolean + default: true + valueExpression: + oneOf: + - "$ref": "#/components/schemas/scalarExpression" + - "$ref": "#/components/schemas/spatialLiteral" + - "$ref": "#/components/schemas/typedTemporalLiteral" + scalarOperands: + type: array + minItems: 2 + maxItems: 2 + items: + "$ref": "#/components/schemas/scalarExpression" + scalarExpression: + oneOf: + - "$ref": "#/components/schemas/scalarLiteral" + - "$ref": "#/components/schemas/propertyRef" + isNullPredicate: + type: object + required: + - isNull + properties: + isNull: + "$ref": "#/components/schemas/scalarExpression" + spatialPredicate: + oneOf: + - "$ref": "#/components/schemas/intersectsExpression" + intersectsExpression: + type: object + required: + - intersects + properties: + intersects: + "$ref": "#/components/schemas/spatialOperands" + spatialOperands: + type: array + minItems: 2 + maxItems: 2 + items: + "$ref": "#/components/schemas/geomExpression" + geomExpression: + oneOf: + - "$ref": "#/components/schemas/spatialLiteral" + - "$ref": "#/components/schemas/propertyRef" + temporalPredicate: + oneOf: + - "$ref": "#/components/schemas/anyinteractsExpression" + anyinteractsExpression: + type: object + required: + - anyinteracts + properties: + anyinteracts: + "$ref": "#/components/schemas/temporalOperands" + temporalOperands: + type: array + minItems: 2 + maxItems: 2 + items: + "$ref": "#/components/schemas/temporalExpression" + temporalExpression: + oneOf: + - "$ref": "#/components/schemas/temporalLiteral" + - "$ref": "#/components/schemas/propertyRef" + propertyRef: + type: object + required: + - property + properties: + propertyName: + type: string + scalarLiteral: + oneOf: + - type: string + - type: number + - type: boolean + spatialLiteral: + oneOf: + - "$ref": "#/components/schemas/geometryLiteral" + - "$ref": "#/components/schemas/envelopeLiteral" + geometryLiteral: + oneOf: + - "$ref": "#/components/schemas/point" + - "$ref": "#/components/schemas/linestring" + - "$ref": "#/components/schemas/polygon" + - "$ref": "#/components/schemas/multipoint" + - "$ref": "#/components/schemas/multilinestring" + - "$ref": "#/components/schemas/multipolygon" + geojson-bbox: + type: array + minItems: 4 + maxItems: 4 + items: + type: number + point: + title: GeoJSON Point + type: object + required: + - type + - coordinates + properties: + type: + type: string + enum: + - Point + coordinates: + type: array + minItems: 2 + items: + type: number + bbox: + "$ref": "#/components/schemas/geojson-bbox" + linestring: + title: GeoJSON LineString + type: object + required: + - type + - coordinates + properties: + type: + type: string + enum: + - LineString + coordinates: + type: array + minItems: 2 + items: + type: array + minItems: 2 + items: + type: number + bbox: + "$ref": "#/components/schemas/geojson-bbox" + polygon: + title: GeoJSON Polygon + type: object + required: + - type + - coordinates + properties: + type: + type: string + enum: + - Polygon + coordinates: + type: array + items: + type: array + minItems: 4 + items: + type: array + minItems: 2 + items: + type: number + bbox: + "$ref": "#/components/schemas/geojson-bbox" + multipoint: + title: GeoJSON MultiPoint + type: object + required: + - type + - coordinates + properties: + type: + type: string + enum: + - MultiPoint + coordinates: + type: array + items: + type: array + minItems: 2 + items: + type: number + bbox: + "$ref": "#/components/schemas/geojson-bbox" + multilinestring: + title: GeoJSON MultiLineString + type: object + required: + - type + - coordinates + properties: + type: + type: string + enum: + - MultiLineString + coordinates: + type: array + items: + type: array + minItems: 2 + items: + type: array + minItems: 2 + items: + type: number + bbox: + "$ref": "#/components/schemas/geojson-bbox" + multipolygon: + title: GeoJSON MultiPolygon + type: object + required: + - type + - coordinates + properties: + type: + type: string + enum: + - MultiPolygon + coordinates: + type: array + items: + type: array + items: + type: array + minItems: 4 + items: + type: array + minItems: 2 + items: + type: number + bbox: + "$ref": "#/components/schemas/geojson-bbox" + envelopeLiteral: + type: object + required: + - bbox + properties: + bbox: + "$ref": "#/components/schemas/bbox" + bbox: + type: array + oneOf: + - minItems: 4 + maxItems: 4 + - minItems: 6 + maxItems: 6 + items: + type: number + temporalLiteral: + oneOf: + - "$ref": "#/components/schemas/timeString" + - "$ref": "#/components/schemas/periodString" + timeString: + oneOf: + - type: string + format: date + - type: string + format: date-time + periodString: + type: array + minItems: 2 + maxItems: 2 + items: + oneOf: + - type: string + format: date + - type: string + format: date-time + - type: string + enum: + - .. + typedTemporalLiteral: + oneOf: + - "$ref": "#/components/schemas/typedTimeString" + - "$ref": "#/components/schemas/typedPeriodString" + typedTimeString: + type: object + required: + - datetime + properties: + datetime: + "$ref": "#/components/schemas/timeString" + typedPeriodString: + type: object + required: + - datetime + properties: + datetime: + "$ref": "#/components/schemas/periodString" diff --git a/fragments/filter/openapi.yaml b/fragments/filter/openapi.yaml new file mode 100644 index 00000000..16872695 --- /dev/null +++ b/fragments/filter/openapi.yaml @@ -0,0 +1,143 @@ +openapi: 3.0.3 +info: + title: The SpatioTemporal Asset Catalog API - Filter + description: Adds parameters to compare properties and only return the items that match + version: 1.0.0-beta.1 + +paths: + "/": + description: Landing Page + get: + responses: + '200': + description: Landing Page + links: + queryables: + operationId: getQueryables + description: |- + A link with rel=queryables for the entire catalog. + + /queryables: + get: + summary: Get the JSON Schema defining the list of variable terms that can be used in CQL expressions. + operationId: getQueryables + description: |- + This endpoint returns a list of variable terms that can be used in CQL expressions. The + precise definition of this can be found in the OGC API - Features - Part 3: Filtering and the + Common Query Language (CQL) specification. + tags: + - Queryables + # parameters: + # todo: may have collections parameter in the future + responses: + '200': + description: A JSON Schema defining the Queryables allowed in CQL expressions + content: + application/schema+json: + schema: + $ref: 'https://json-schema.org/draft/2019-09/schema' + default: + $ref: '../../core/commons.yaml#/components/responses/Error' + /collections/{collectionId}: + get: + responses: + '200': + description: Collection description + links: + queryables: + operationId: getQueryables + description: |- + A link with rel=queryables for queryables to only apply to this collection. + /collections/{collectionId}/queryables: + get: + summary: Get the JSON Schema defining the list of variable terms that can be used in CQL expressions. + operationId: getQueryablesForCollection + description: |- + This endpoint returns a list of variable terms that can be used in CQL expressions. The + precise definition of this can be found in the OGC API - Features - Part 3: Filtering and the + Common Query Language (CQL) specification. + tags: + - Queryables + responses: + '200': + description: A JSON Schema defining the Queryables allowed in CQL expressions filtering only that collection + content: + application/schema+json: + schema: + $ref: 'https://json-schema.org/draft/2019-09/schema' + default: + $ref: '../../core/commons.yaml#/components/responses/Error' +components: + parameters: + filter: + name: filter + x-stac-api-fragment: filter + in: query + description: |- + **Extension:** Filter + + A CQL filter expression for filtering items. + required: true + schema: + oneOf: + - $ref: '#/components/schemas/filter-cql-json' + - $ref: '#/components/schemas/filter-cql-text' + filter-lang: + name: filter-lang + x-stac-api-fragment: filter + in: query + description: |- + **Extension:** Filter + + The CQL filter encoding that the 'filter' value uses. Must be one of 'cql-text' or 'cql-json'. + required: false + schema: + $ref: '#/components/schemas/filter-lang' + filter-crs: + name: filter-crs + x-stac-api-fragment: filter + in: query + description: |- + **Extension:** Filter + + The CRS used by spatial predicates in the filter parameter. In STAC API, only value that must be accepted + is 'http://www.opengis.net/def/crs/OGC/1.3/CRS84'. + required: false + schema: + $ref: '#/components/schemas/filter-crs' + schemas: + searchBody: + type: object + x-stac-api-fragment: filter + description: |- + **Optional Extension:** Filter + + A filter for properties in Items. + properties: + filter: + $ref: '#/components/schemas/filter-cql-json' + filter-lang: + $ref: '#/components/schemas/filter-lang' + filter-crs: + $ref: '#/components/schemas/filter-crs' + filter-cql-text: + description: | + A CQL filter expression in the 'cql-text' encoding. + type: string + filter-cql-json: + description: | + A CQL filter expression in the 'cql-json' encoding. + $ref: './cql.yml#/components/schemas/booleanValueExpression' + filter-lang: + description: | + The CQL filter encoding that the 'filter' value uses. + type: string + enum: + - 'cql-text' + - 'cql-json' + filter-crs: + description: | + The coordinate reference system (CRS) used by spatial literals in the 'filter' value. The only value that STAC APIs must + accept is 'http://www.opengis.net/def/crs/OGC/1.3/CRS84'. + type: string + format: uri \ No newline at end of file diff --git a/fragments/query/README.md b/fragments/query/README.md index 2c3babc0..4a7bcacf 100644 --- a/fragments/query/README.md +++ b/fragments/query/README.md @@ -1,8 +1,9 @@ # STAC API - Query Fragment - **OpenAPI specification:** [openapi.yaml](openapi.yaml) -- **Extension [Maturity Classification](../../extensions.md#extension-maturity):** Pilot - Likely to get deprecated in the future in favor of [CQL](http://docs.opengeospatial.org/DRAFTS/19-079.html). +- **Conformance Class:** +- **Extension [Maturity Classification](../../extensions.md#extension-maturity):** Deprecated in + favor of the [Filter Extension](../filter/README.md) using [CQL](http://docs.opengeospatial.org/DRAFTS/19-079.html). - **Dependents:** - [Item Search](../../item-search) diff --git a/fragments/sort/README.md b/fragments/sort/README.md index 8eaf5e5c..6fd1c948 100644 --- a/fragments/sort/README.md +++ b/fragments/sort/README.md @@ -1,6 +1,7 @@ # STAC API - Sort Fragment - **OpenAPI specification:** [openapi.yaml](openapi.yaml) +- **Conformance Class:** - **Fragment [Maturity Classification](../../extensions.md#extension-maturity):** Pilot - **Dependents:** - [Item Search](../../item-search) diff --git a/implementation.md b/implementation.md index 5b6e955c..dd81ebcf 100644 --- a/implementation.md +++ b/implementation.md @@ -10,14 +10,18 @@ configuration so browser-based UIs running on a different domain may more easily APIs should acknowledge pre-flight request headers. In general, these header values should be set on responses: -``` +```http access-control-allow-origin: * access-control-allow-methods: OPTIONS, POST, GET access-control-allow-headers: Content-Type ``` -It is relatively safe to use these headers for all endpoints. However, one may want to restrict the methods to only those that apply to each endpoint. For example, the `/collection/{collectionId}/items` endpoint should only allow OPTIONS and GET, since POST is only used by the Transactions Extension, which presumably would require authentication as it is mutating data. +It is relatively safe to use these headers for all endpoints. However, one may want to restrict the methods to +only those that apply to each endpoint. For example, the `/collection/{collectionId}/items` endpoint should +only allow OPTIONS and GET, since POST is only used by the Transactions Extension, which presumably would +require authentication as it is mutating data. Implementations that support the Transactions Extension or require credentials for some operations will need to -implement different behavior, for example, allowing credentials when requests are coming from a trusted domain, allowing DELETE, PUT, or PATCH methods, or +implement different behavior, for example, allowing credentials when requests are coming from a trusted domain, +allowing DELETE, PUT, or PATCH methods, or permitting the `If-Match` request header. diff --git a/item-search/README.md b/item-search/README.md index be46c727..15e1d2ed 100644 --- a/item-search/README.md +++ b/item-search/README.md @@ -14,9 +14,10 @@ - [Example Landing Page for STAC API - Item Search](#example-landing-page-for-stac-api---item-search) - [Extensions](#extensions) - [Fields](#fields) - - [Query](#query) + - [Filter](#filter) - [Sort](#sort) - [Context](#context) + - [Query](#query) - **OpenAPI specification:** [openapi.yaml](openapi.yaml) ([rendered version](https://api.stacspec.org/v1.0.0-beta.1/item-search)) - **Conformance URI:** @@ -271,17 +272,17 @@ allows the client to suggest to the server which Item attributes should be inclu through the use of a `fields` parameter. The full description of how this extension works can be found in the [fields fragment](../fragments/fields/). -### Query +### Filter -- **Conformance URI:** +- **Conformance URI:** - **Extension [Maturity Classification](../extensions.md#extension-maturity):** Pilot -- **Definition**: [STAC API - Query Fragment](../fragments/query/) +- **Definition**: [STAC API - Filter Fragment](../fragments/filter/) The STAC search endpoint, `/search`, by default only accepts a limited set of parameters to limit the results -by properties. The Query extension adds a new parameter, `query`, that can take a number of comparison operators to -match predicates between the fields requested and the values of Item objects. It can be used with both GET and POST, though -GET includes the exact same JSON. The full details on the JSON structure are specified in the [query -fragment](../fragments/query/). +by properties. The Filter extension adds a new parameter, `filter`, that can take a number of comparison operators to +match predicates between the fields requested and the values of Item objects. It can be used with both GET and POST and supports two +query formats, `cql-text` and `cql-json`. The full details on the JSON structure are specified in the [filter +fragment](../fragments/filter/). ### Sort @@ -305,3 +306,18 @@ of this extension can be found in the [sort fragment](../fragments/sort). This extension is intended to augment the core ItemCollection responses from the `search` API endpoint with a JSON object called `context` that includes the number of items `matched`, `returned` and the `limit` requested. The full description and examples of this are found in the [context fragment](../fragments/context). + +### Query + +- **Conformance URI:** +- **Extension [Maturity Classification](../extensions.md#extension-maturity):** Deprecated +- **Definition**: [STAC API - Query Fragment](../fragments/query/) + +**Note** - the Query Extension is deprecated as of 1.0.0. Implementers +are encouraged to use the Filter Extension instead. + +The STAC search endpoint, `/search`, by default only accepts a limited set of parameters to limit the results +by properties. The Query extension adds a new parameter, `query`, that can take a number of comparison operators to +match predicates between the fields requested and the values of Item objects. It can be used with both GET and POST, though +GET includes the exact same JSON. The full details on the JSON structure are specified in the [query +fragment](../fragments/query/). diff --git a/item-search/openapi.yaml b/item-search/openapi.yaml index e6bfe40b..d364dda8 100644 --- a/item-search/openapi.yaml +++ b/item-search/openapi.yaml @@ -34,13 +34,14 @@ paths: - Item Search parameters: - $ref: '#/components/parameters/bbox' + - $ref: '#/components/parameters/intersects' - $ref: '#/components/parameters/datetime' - $ref: '#/components/parameters/limit' - $ref: '#/components/parameters/ids' - $ref: '#/components/parameters/collectionsArray' # extensions - $ref: '../fragments/fields/openapi.yaml#/components/parameters/fields' - - $ref: '../fragments/query/openapi.yaml#/components/parameters/query' + - $ref: '../fragments/filter/openapi.yaml#/components/parameters/filter' - $ref: '../fragments/sort/openapi.yaml#/components/parameters/sortby' responses: '200': @@ -78,7 +79,7 @@ paths: - $ref: '#/components/schemas/searchBody' # extensions - $ref: '../fragments/fields/openapi.yaml#/components/schemas/searchBody' - - $ref: '../fragments/query/openapi.yaml#/components/schemas/searchBody' + - $ref: '../fragments/filter/openapi.yaml#/components/schemas/searchBody' - $ref: '../fragments/sort/openapi.yaml#/components/schemas/searchBody' responses: '200': @@ -203,6 +204,17 @@ components: default: 10 style: form explode: false + intersects: + name: intersects + in: query + description: |- + The optional intersects parameter filters the result Items in the same was as bbox, only with + a GeoJSON Geometry rather than a bbox. + required: false + schema: + $ref: "../core/commons.yaml#/components/schemas/geometryGeoJSON" + style: form + explode: false schemas: searchBody: description: The search criteria diff --git a/ogcapi-features/README.md b/ogcapi-features/README.md index 54b5f254..89c69cbf 100644 --- a/ogcapi-features/README.md +++ b/ogcapi-features/README.md @@ -112,7 +112,6 @@ most APIs will also implement other conformance classes, and those will be refle `conformsTo` fields. A more typical Landing Page example is in the [overview](../overview.md#example-landing-page) document. - ```json { "stac_version": "1.0.0-beta.2", @@ -159,4 +158,4 @@ the [overview](../overview.md#example-landing-page) document. } ] } -``` \ No newline at end of file +``` diff --git a/overview.md b/overview.md index cda38b75..eaee28f9 100644 --- a/overview.md +++ b/overview.md @@ -131,7 +131,8 @@ Additional conformance classes are specified in the [STAC Extensions](extensions ## Example Landing Page When all three conformance classes (Core, Features, Item Search) are implemented, the relationships among -various resources are shown in the following diagram. In each node, there is also a `self` and `root` link that are not depicted to keep the diagram more concise. +various resources are shown in the following diagram. In each node, there is also a `self` and `root` link +that are not depicted to keep the diagram more concise. ![Diagram of STAC link relations](stac-api.png)