STAC Client Implementation Outline#

Client Search Scenario#

Connected Data Asset STAC clients are, at their core, just conventional STAC clients respecting a number of Best Practices to make the STAC API protocol useful in the WGISS environment. The collection identifier of the collection of interest is a mandatory element in a CEOS STAC request to find granules (a.k.a. STAC items). Clients could retrieve collection IDs from the IDN or FedEO Collection search responses.

This chapter will give brief steps about how to retrieve a collection ID and how to interact with the granule search server for inventory search. The corresponding details are elaborated in the Use Case chapters.

We assume that Data providers have registered the metadata of their archived collections into the IDN or have made their collections accessible to FedEO. The client can query the IDN or FedEO to retrieve the collection ID for a desired collection of interest and, based on that collection ID and other spatial-temporal query conditions, build a valid CEOS STAC query. The following steps describe the client search scenario starting with a STAC Collection search.

URL_LANDING_PAGE =  'https://fedeo.ceos.org/' 

Step 1

Obtain the STAC API Collection search endpoint to formulate a valid Collection search request.

The API implements the STAC API Collection Search Extension [RD25]. The URL of the collection search endpoint is advertized in the Landing Page as a link (rel=”data”). The code below extracts this link from the FedEO landing page.

from jsonpath_ng.ext import parse

response = requests.get(URL_LANDING_PAGE)
data = json.loads(response.text)
expression = parse("$.links[?(@.rel == 'data')].href")
r = expression.find(data)
r[0].value
'https://fedeo.ceos.org/collections'

The link with rel=”http://www.opengis.net/def/rel/ogc/1.0/queryables” provides access to the list of filter criteria available for collection search. It returns a Queryables object in JSON Schema format. The list of filter criteria IDN and FedEO support for collection searches differs.

# retrieve /collections response
response = requests.get(r[0].value)
data = json.loads(response.text)
from jsonpath_ng.ext import parse

expression = parse("$.links[?(@.rel == 'http://www.opengis.net/def/rel/ogc/1.0/queryables')].href")
r = expression.find(data)
r[0].value
'https://fedeo.ceos.org/collections/queryables'
# Get queryables response and list parameters alphabetically.
response = requests.get(r[0].value)
data = json.loads(response.text)    
df = pd.DataFrame(data['properties'].items(),columns=['key','value'])
df['type'] = df.apply(lambda row : row['value']['type'], axis = 1)
df['format'] = df.apply(lambda row : row['value']['format'] if 'format' in row['value'] else '-' , axis = 1)
df.drop('value',axis=1).sort_values(by=['key'])
key type format
14 classifiedAs string uri
13 doi string -
3 instrument string -
8 modificationDate string date-time
11 offering string -
7 organisationName string -
5 otherConstraint string -
2 parentIdentifier string -
6 platform string -
9 processingLevel string -
12 publisher string -
1 query string -
0 subject string -
4 title string -
10 useLimitation string -

Step 2

Search collections of interest through STAC with proper request parameters (e.g. spatial footprint, temporal extent and keyword).

The collection search supports free text searches using the q query parameter as shown below.

from pystac_client import Client
api = Client.open(URL_LANDING_PAGE)

results = api.collection_search(
    q = 'Proba-1'
)
curl -X GET -G https://fedeo.ceos.org/collections \
	--data-urlencode "q=Proba-1"
data = results.collection_list_as_dict()
df = pd.json_normalize(data, record_path=['collections'])
df[['id', 'keywords']]
id keywords
0 PROBA.CHRIS.1A [DIF10, EARTH SCIENCE>BIOSPHERE>ECOSYSTEMS>TER...
1 PROBA.HRC.1A [DIF10, EARTH SCIENCE>BIOSPHERE>ECOSYSTEMS>TER...

Step 3

From the collection search response, obtain the granule search endpoint for this collection, or obtain the collection ID to be used in a cross-collection search request.

The cross collection granule search endpoint is advertized in the Landing Page as a link with rel=”search”.

from jsonpath_ng.ext import parse

# find the link with rel="search" and method="GET"

response = requests.get(URL_LANDING_PAGE)
data = json.loads(response.text)
expression = parse("$.links[?(@.method == 'GET' & @.rel == 'search' )].href")
r = expression.find(data)
r[0].value
'https://fedeo.ceos.org/search'

The granule search endpoint for the individual collection is advertized in the collection metadata as a link with rel=”items”. The collection metadata can be found inside a collection search response or be accessed directly if its collection ID is known. The server may support both the GET and POST methods.

For example, the collection metadata for PROBA.CHRIS.1A, is available at at https://fedeo.ceos.org/collections/PROBA.CHRIS.1A. This corresponds to one of the many representations available using content-negotiation.

URL = URL_LANDING_PAGE + 'collections/' + COLLECTION_ID1
curl -X GET -G https://fedeo.ceos.org/collections/PROBA.CHRIS.1A
'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items?httpAccept=application/geo%2Bjson;profile=https://stacspec.org'

Step 4

Based on the common query paramters or on retrieved Queryables, formulate a STAC API search request for granules belonging to that collection, directed to the cross-collection search endpoint.

from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'GET',         
    max_items = 2,
    collections=[COLLECTION_ID1],
    bbox = [14.90, 37.700, 14.99, 37.780], # Mount Etna
    datetime=['2015-01-01T00:00:00Z', '2022-01-02T00:00:00Z']
)
curl -X GET -G https://fedeo.ceos.org/search \
	--data-urlencode "bbox=14.9,37.7,14.99,37.78" \
	--data-urlencode "datetime=2015-01-01T00:00:00Z/2022-01-02T00:00:00Z" \
	--data-urlencode "collections=PROBA.CHRIS.1A"
print(f"{results.matched()} items found.")
9 items found.
# Show search response (GeoJSON)
data = results.item_collection_as_dict()
jstr = json.dumps(data, indent=3)
md("```json\n" + jstr + "\n```\n")

Obtaining the Queryables Document#

A Queryables document provides necessary information for clients to programmatically formulate valid search requests using the filter parameter. Specifically, clients are expected to acquire both the name and type of the parameters based on the JSON Schema returned by the /queryables endpoint.

The list of supported filter parameters (a.k.a. queryables) for this collection can be retrieved from the collection metadata as shown below.

# extract the queryables endpoint

expression = parse("$.links[?(@.rel == 'http://www.opengis.net/def/rel/ogc/1.0/queryables' )].href")
r = expression.find(data_collections)
r[0].value
'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/queryables'

At this endpoint, the list of filter parameters is available as a JSON Schema. The table below shows the filter parameters for this specific collection.

response = requests.get(r[0].value)   
data = json.loads(response.text)    
df = pd.DataFrame(data['properties'].items(),columns=['key','value'])
df['type'] = df.apply(lambda row : row['value']['type'], axis = 1)
df['format'] = df.apply(lambda row : row['value']['format'] if 'format' in row['value'] else '-' , axis = 1)
df.drop('value',axis=1).sort_values(by=['key'])
key type format
0 acquisitionType string -
5 availabilityTime string -
13 frame string -
1 illuminationAzimuthAngle number -
11 illuminationElevationAngle number -
2 instrument string -
7 modificationDate string date-time
4 orbitNumber integer -
3 platform string -
6 platformSerialIdentifier string -
12 productType string -
8 productionStatus string -
10 sensorMode string -
9 track string -

Search request#

The WGISS CDA support both searching for collections through the IDN or FedEO and for granules in a specific collection at one of the data partners. It executes a collection or inventory search (a.k.a. granule search), as appropriate, and returns the matching results.

Search response#

The STAC API returns the search responses in (Geo)JSON formats.

The GeoJSON response elements include:

  • numberReturned: number of results returned in current response page

  • numberMatched: number of total results

print(f"{results.matched()} items found.")
9 items found.
data_granules
{'type': 'FeatureCollection',
 'features': [{'stac_version': '1.0.0',
   'assets': {'thumbnail': {'roles': ['thumbnail'],
     'href': 'http://tpm-ds.eo.esa.int/oads/meta/PROBA1-CHRIS/thumbnail/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001.SIP.ZIP_TIMG.jpg',
     'type': 'image/jpeg',
     'title': 'THUMBNAIL'},
    'enclosure': {'roles': ['data'],
     'href': 'https://tpm-ds.eo.esa.int/oads/data/PROBA1-CHRIS/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001.SIP.ZIP',
     'type': 'application/zip',
     'title': 'Download',
     'file:size': 257077674},
    'metadata_ogc_10_157r4': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/gml%2Bxml&recordSchema=om',
     'title': 'OGC 10-157r4 metadata',
     'type': 'application/gml+xml;profile="http://www.opengis.net/spec/EOMPOM/1.1"'},
    'metadata_ogc_17_069r3': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001',
     'title': 'OGC 17-069r3 metadata',
     'type': 'application/geo+json;profile="http://www.opengis.net/spec/ogcapi-features-1/1.0"'},
    'metadata_ogc_17_003r2': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?mode=owc',
     'title': 'OGC 17-003r2 metadata',
     'type': 'application/geo+json;profile="http://www.opengis.net/spec/eo-geojson/1.0"'},
    'metadata_iso_19139': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/vnd.iso.19139%2Bxml',
     'title': 'ISO 19139 metadata',
     'type': 'application/vnd.iso.19139+xml'},
    'metadata_html': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=text/html',
     'title': 'HTML',
     'type': 'text/html'},
    'quicklook': {'roles': ['overview'],
     'href': 'http://tpm-ds.eo.esa.int/oads/meta/PROBA1-CHRIS/browse/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001.SIP.ZIP_BID.PNG',
     'type': 'image/png',
     'title': 'QUICKLOOK'}},
   'bbox': [14.94, 37.69, 15.09, 37.79],
   'geometry': {'coordinates': [[[15.08, 37.71],
      [15.09, 37.77],
      [14.94, 37.79],
      [14.96, 37.69],
      [15.08, 37.71]]],
    'type': 'Polygon'},
   'links': [{'rel': 'self',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/geo%2Bjson;profile=https://stacspec.org',
     'type': 'application/geo+json;profile="https://stacspec.org"'},
    {'rel': 'collection',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A?httpAccept=application/json',
     'type': 'application/json',
     'title': 'PROBA.CHRIS.1A'},
    {'rel': 'parent',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A?httpAccept=application/json',
     'type': 'application/json',
     'title': 'PROBA.CHRIS.1A'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/atom%2Bxml',
     'type': 'application/atom+xml',
     'title': 'Atom format'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/ld%2Bjson',
     'type': 'application/ld+json',
     'title': 'JSON-LD metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/ld%2Bjson;profile=https://schema.org',
     'type': 'application/ld+json;profile="https://schema.org"',
     'title': 'JSON-LD (schema.org) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/ld%2Bjson;profile=http://data.europa.eu/930/',
     'type': 'application/ld+json;profile="http://data.europa.eu/930/"',
     'title': 'JSON-LD (GeoDCAT-AP) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/rdf%2Bxml',
     'type': 'application/rdf+xml',
     'title': 'RDF/XML metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/rdf%2Bxml;profile=https://schema.org',
     'type': 'application/rdf+xml;profile="https://schema.org"',
     'title': 'RDF/XML (schema.org) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=application/rdf%2Bxml;profile=http://data.europa.eu/930/',
     'type': 'application/rdf+xml;profile="http://data.europa.eu/930/"',
     'title': 'RDF/XML (GeoDCAT-AP) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=text/turtle',
     'type': 'text/turtle',
     'title': 'Turtle metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=text/turtle;profile=https://schema.org',
     'type': 'text/turtle;profile="https://schema.org"',
     'title': 'Turtle (schema.org) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001?httpAccept=text/turtle;profile=http://data.europa.eu/930/',
     'type': 'text/turtle;profile="http://data.europa.eu/930/"',
     'title': 'Turtle (GeoDCAT-AP) metadata'}],
   'id': 'PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001',
   'collection': 'PROBA.CHRIS.1A',
   'type': 'Feature',
   'stac_extensions': ['https://stac-extensions.github.io/file/v2.1.0/schema.json',
    'https://stac-extensions.github.io/projection/v1.1.0/schema.json',
    'https://stac-extensions.github.io/view/v1.0.0/schema.json',
    'https://stac-extensions.github.io/grid/v1.1.0/schema.json',
    'https://stac-extensions.github.io/product/v1.0.0/schema.json',
    'https://stac-extensions.github.io/timestamps/v1.1.0/schema.json'],
   'properties': {'grid:code': 'WRS-E015-N37',
    'proj:code': 'EPSG:4326',
    'time:published': '2017-04-19T13:02:00Z',
    'start_datetime': '2017-04-19T13:58:00Z',
    'end_datetime': '2017-04-19T13:02:00Z',
    'view:sun_azimuth': 18.74868429130781,
    'product:type': 'CHR_MO1_1P',
    'title': 'PR1_OPER_CHR_MO1_1P_20170419T135800_N37-075_E015-001_0001',
    'platform': 'PROBA-1',
    'view:sun_elevation': 48.013,
    'datetime': '2017-04-19T13:58:00Z',
    'sar:instrument_mode': 'MODE-1',
    'instruments': ['CHRIS'],
    'constellation': 'PROBA',
    'updated': '2026-05-29T18:11:02Z',
    'product:acquisition_type': 'nominal'}},
  {'stac_version': '1.0.0',
   'assets': {'thumbnail': {'roles': ['thumbnail'],
     'href': 'http://tpm-ds.eo.esa.int/oads/meta/PROBA1-CHRIS/thumbnail/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001.SIP.ZIP_TIMG.jpg',
     'type': 'image/jpeg',
     'title': 'THUMBNAIL'},
    'enclosure': {'roles': ['data'],
     'href': 'https://tpm-ds.eo.esa.int/oads/data/PROBA1-CHRIS/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001.SIP.ZIP',
     'type': 'application/zip',
     'title': 'Download',
     'file:size': 259100438},
    'metadata_ogc_10_157r4': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/gml%2Bxml&recordSchema=om',
     'title': 'OGC 10-157r4 metadata',
     'type': 'application/gml+xml;profile="http://www.opengis.net/spec/EOMPOM/1.1"'},
    'metadata_ogc_17_069r3': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001',
     'title': 'OGC 17-069r3 metadata',
     'type': 'application/geo+json;profile="http://www.opengis.net/spec/ogcapi-features-1/1.0"'},
    'metadata_ogc_17_003r2': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?mode=owc',
     'title': 'OGC 17-003r2 metadata',
     'type': 'application/geo+json;profile="http://www.opengis.net/spec/eo-geojson/1.0"'},
    'metadata_iso_19139': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/vnd.iso.19139%2Bxml',
     'title': 'ISO 19139 metadata',
     'type': 'application/vnd.iso.19139+xml'},
    'metadata_html': {'roles': ['metadata'],
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=text/html',
     'title': 'HTML',
     'type': 'text/html'},
    'quicklook': {'roles': ['overview'],
     'href': 'http://tpm-ds.eo.esa.int/oads/meta/PROBA1-CHRIS/browse/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001.SIP.ZIP_BID.PNG',
     'type': 'image/png',
     'title': 'QUICKLOOK'}},
   'bbox': [14.94, 37.69, 15.09, 37.79],
   'geometry': {'coordinates': [[[15.08, 37.71],
      [15.09, 37.77],
      [14.94, 37.79],
      [14.96, 37.69],
      [15.08, 37.71]]],
    'type': 'Polygon'},
   'links': [{'rel': 'self',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/geo%2Bjson;profile=https://stacspec.org',
     'type': 'application/geo+json;profile="https://stacspec.org"'},
    {'rel': 'collection',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A?httpAccept=application/json',
     'type': 'application/json',
     'title': 'PROBA.CHRIS.1A'},
    {'rel': 'parent',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A?httpAccept=application/json',
     'type': 'application/json',
     'title': 'PROBA.CHRIS.1A'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/atom%2Bxml',
     'type': 'application/atom+xml',
     'title': 'Atom format'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/ld%2Bjson',
     'type': 'application/ld+json',
     'title': 'JSON-LD metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/ld%2Bjson;profile=https://schema.org',
     'type': 'application/ld+json;profile="https://schema.org"',
     'title': 'JSON-LD (schema.org) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/ld%2Bjson;profile=http://data.europa.eu/930/',
     'type': 'application/ld+json;profile="http://data.europa.eu/930/"',
     'title': 'JSON-LD (GeoDCAT-AP) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/rdf%2Bxml',
     'type': 'application/rdf+xml',
     'title': 'RDF/XML metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/rdf%2Bxml;profile=https://schema.org',
     'type': 'application/rdf+xml;profile="https://schema.org"',
     'title': 'RDF/XML (schema.org) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=application/rdf%2Bxml;profile=http://data.europa.eu/930/',
     'type': 'application/rdf+xml;profile="http://data.europa.eu/930/"',
     'title': 'RDF/XML (GeoDCAT-AP) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=text/turtle',
     'type': 'text/turtle',
     'title': 'Turtle metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=text/turtle;profile=https://schema.org',
     'type': 'text/turtle;profile="https://schema.org"',
     'title': 'Turtle (schema.org) metadata'},
    {'rel': 'alternate',
     'href': 'https://fedeo.ceos.org/collections/PROBA.CHRIS.1A/items/PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001?httpAccept=text/turtle;profile=http://data.europa.eu/930/',
     'type': 'text/turtle;profile="http://data.europa.eu/930/"',
     'title': 'Turtle (GeoDCAT-AP) metadata'}],
   'id': 'PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001',
   'collection': 'PROBA.CHRIS.1A',
   'type': 'Feature',
   'stac_extensions': ['https://stac-extensions.github.io/file/v2.1.0/schema.json',
    'https://stac-extensions.github.io/projection/v1.1.0/schema.json',
    'https://stac-extensions.github.io/view/v1.0.0/schema.json',
    'https://stac-extensions.github.io/grid/v1.1.0/schema.json',
    'https://stac-extensions.github.io/product/v1.0.0/schema.json',
    'https://stac-extensions.github.io/timestamps/v1.1.0/schema.json'],
   'properties': {'grid:code': 'WRS-E015-N37',
    'proj:code': 'EPSG:4326',
    'time:published': '2016-04-13T14:48:00Z',
    'start_datetime': '2016-04-13T14:44:00Z',
    'end_datetime': '2016-04-13T14:48:00Z',
    'view:sun_azimuth': 32.76774763398471,
    'product:type': 'CHR_MO1_1P',
    'title': 'PR1_OPER_CHR_MO1_1P_20160413T144400_N37-075_E015-001_0001',
    'platform': 'PROBA-1',
    'view:sun_elevation': 57.681,
    'datetime': '2016-04-13T14:44:00Z',
    'sar:instrument_mode': 'MODE-1',
    'instruments': ['CHRIS'],
    'constellation': 'PROBA',
    'updated': '2026-05-29T18:11:02Z',
    'product:acquisition_type': 'nominal'}}]}

Example: 2.1

Search granules by geometry {intersects} [RD11] and GET method. Geometry parameter can be provided as dictionary or string.

The total number of results available is reported in the numberMatched property.

Example: 2.2

Search granules by geometry {intersects} [RD11] and POST method. Geometry parameter can be provided as dictionary or string.

# same request with POST
from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'POST',        
    max_items = 2,
    collections=[COLLECTION_ID1],
    # intersects = json.dumps(aoi_as_dict), 
    intersects = aoi_as_dict,
    datetime=['2015-01-01T00:00:00Z', '2022-01-02T00:00:00Z']
)
print(f"{results.matched()} items found.")

Search by bounding box#

The geometry parameter can be provided as Python list or tuple.

Example: 2.3

Search granules by bounding box {bbox} list [RD11]. bbox parameter is provided as Python list.

from pystac_client import Client
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'GET',   
    max_items=10,
    collections=[COLLECTION_ID1],
    bbox = [14.90, 37.700, 14.99, 37.780], # Mount Etna
    # datetime=['2015-01-01T00:00:00Z', '2022-01-02T00:00:00Z']
)

Same request using curl.

Example: 2.4

Search granules by bounding box {bbox} [RD11]. Geometry parameter is provided as Python tuple.

# x, y = (14.95, 37.74)   # Center point of query (Mount Etna)
x, y = (4.38, 51.25)   # Center point of query (Antwerp harbour) 

r = 0.1
box = (x - r, y - r, x + r, y + r)

from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'GET',   
    max_items=10,
    collections=[COLLECTION_ID1],
    bbox = box
)

Same request using curl.

Example: 2.5

Search granules by bounding box (bbox) and generate density map.

collection_id = COLLECTION_ID1

def get_results(bbox):
    x, y, x2, y2 = bbox
    results = api.search(
            method = 'GET',   
            max_items=1, 
            bbox = [x, y, x2, y2], 
            collections=[collection_id]
    ) 
    return results.matched()

collection_size = get_results([-180, -90, 180, 90])
n_rows = 18
n_columns = 36

dy = 180.0 / n_rows
dx = 360.0 / n_columns
shape = (n_rows, n_columns)
Z = np.zeros(shape)

bboxes = []
for col in range(n_columns):
    for row in range(n_rows):
        x = col * dx - 180.0
        y = row * dy - 90.0
        bboxes.append((x, y, x+dx, y+dy))
%%time
executor = ThreadPoolExecutor(max_workers=32)

results = executor.map(get_results, bboxes)

for col in range(n_columns):
    for row in range(n_rows):
        count = next(results)
        Z[row, col] = count

Search by temporal extent#

Example: 2.6

Search granules by date range (datetime) [RD01].

from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'GET',   
    max_items = 50,
    collections=[COLLECTION_ID1],
    datetime=['2019-01-01T00:00:00Z', '2019-12-02T00:00:00Z']
)
display_date_distribution(results)

Example: 2.7

Search granules by open-ended date range (datetime) [RD01].

from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'GET',   
    max_items = 50,
    collections=[COLLECTION_ID1],
    datetime=['2021-12-01T00:00:00Z', None]
)
print(f"{results.matched()} items found.")
display_value_distribution(results, 'sar:product_type')

Search by identifier#

Example: 2.8

Search granule by identifier (ids) [RD01].

granule_id1
from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'GET',   
    collections=[COLLECTION_ID1],
    ids=[granule_id1]
)

Search with filter#

Example: 2.10

Search granules with filter {filter} [RD01]. Available filters are advertised in Queryables object at /collections/{id}/queryables.

from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'GET',   
    max_items=10,
    collections=[COLLECTION_ID1],
    bbox = [14.90, 37.700, 14.99, 37.780], # Mount Etna
    datetime=['2015-01-01T00:00:00Z', '2022-01-02T00:00:00Z'],
    filter="productType='CHR_MO2_1P' and instrument='CHRIS'"
)

Same request with curl.

display_value_distribution(results, 'sar:product_type')

Search by cloud cover#

Example: 2.11

Search granules by cloudcover (filter and cloudCover) [RD01]. Available filters are advertised in Queryables object at /collections/{id}/queryables.

from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE)  

results = api.search(
    method = 'GET',   
    max_items=50,
    collections=[COLLECTION_ID3_CLOUDS],
    filter="cloudCover < 10"    
)
# Display cloud-cover values as histogram to show that range is taken into account
stac_json = results.item_collection_as_dict()
gdf = gpd.GeoDataFrame.from_features(stac_json)
try:
  _ = gdf[['title','eo:cloud_cover']].hist()
except:
  print("eo:cloud_cover information is not available.")
# fails if properties are not in the metadata.
try:
  # _ = gdf[['view:sun_elevation','view:incidence_angle','view:sun_azimuth']].plot.hist(alpha=0.7)
  _ = gdf[['view:sun_elevation','view:sun_azimuth']].plot.hist(alpha=0.7)
except:
  print("acquisition angle information is not available.")
# gdf
# display_value_distribution(results, 'sat:orbit_state')
display_value_distribution(results, 'sar:product_type')

Search multiple collections#

Example: 2.12

Search granules in multiple collections {collections} [RD01].

from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'GET',   
    max_items=10,
    collections=[COLLECTION_ID2_CLOUDS, COLLECTION_ID1],
    bbox = [13.90, 36.700, 15.99, 38.780], # Mount Etna (large)
)
print(f"{results.matched()} items found.")

Example: 2.13

Search granules in multiple collections {collections} [RD01] using POST.

from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE) 

results = api.search(
    method = 'POST',    
    max_items=50,
    collections=[COLLECTION_ID2_CLOUDS, COLLECTION_ID1],
    bbox = [13.90, 36.700, 15.99, 38.780] # Mount Etna (large)
)
print(f"{results.matched()} items found.")

Granule properties#

Granules are returned via item links in the Catalog or Collection objects, or via the STAC API (Feature). An item is a GeoJSON Feature and the encoding is derived from the original OGC 17-003r2 encoding according to a documented mapping.

The properties available include attributes from STAC extensions as well:

Assets#

Granules provide access to a dictionary with assets. The roles attribute indicates the purpose of the asset. The href attribute provides the URL to access the asset. Granule assets include thumbnail (when available), a data download link (equivalent to the rel=enclosure), and various metadata formats.

The table below list some frequently used metadata formats and their corresponding media type (type).

Format

type

ISO19139

application/vnd.iso.19139+xml

ISO19139-2

application/vnd.iso.19139-2+xml

ISO19115-3

application/vnd.iso.19115-3+xml

OGC 10-157r4

application/gml+xml;profile=http://www.opengis.net/spec/EOMPOM/1.1

OGC 17-003r2

application/geo+json;profile=http://www.opengis.net/spec/eo-geojson/1.0

# Show assets of first search result (GeoJSON)
data = results.item_collection_as_dict()
jstr = json.dumps(data['features'][1]['assets'], indent=3)
md("```json\n" + jstr + "\n```\n")
df = pd.DataFrame(columns=['roles', 'title', 'type'])
    
# Display assets belonging to first item in results
for item in results.items():
    assets = item.assets
    for key in assets:     
        ndf = pd.DataFrame({ 
            'roles': assets[key].roles, 
            'type': assets[key].media_type, 
            'title': assets[key].title, 
            # 'href': assets[key].href  
        }, index = [0])
        df = pd.concat([df, ndf], ignore_index=True)
    
    break
df

Advanced topics#

Conformance classes#

The conformance classes supported by the STAC interface are advertised in the conformsTo property of the landing page.

Additional search parameters#

URL_QUERYABLES = URL_LANDING_PAGE + 'collections/queryables'
response = requests.get(URL_QUERYABLES)   
data = json.loads(response.text)    
df = pd.DataFrame(data['properties'].items(),columns=['key','value'])
df['type'] = df.apply(lambda row : row['value']['type'], axis = 1)
df['format'] = df.apply(lambda row : row['value']['format'] if 'format' in row['value'] else '-' , axis = 1)
df.drop('value',axis=1).sort_values(by=['key'])

Additional search parameters beyond the STAC search parameters can be used to filter granule search results. The available parameters for granule search are advertised for each individual collection and represented as a JSON Schema.

Get filter parameters for granule search

CQL filter expressions#

The STAC interface supports the filter parameter and filter expressions in cql-text filter format at the following endpoints:

  • /collections

  • /collections/{collection-id}/items

  • /search

At the /search endpoint, it is required that a single collection can be determined from the collections or ids parameter. The queryables allowed in the filter expression are then identical to the ones at the corresponding /collections/{collection-id}/items/queryables endpoint. filter cannot be used at the /search endpoint when collections contains 0 or more than 1 collection identifiers.

Filter expressions are to be expressed with the Text encoding of the Basic Common Query Language (Basic CQL2-Text) [RD22]. See the OGC API Features “Conformance class Filter” section for CQL2 examples.

Example: 8.1

CQL Filter for collection search with logical operators (and, or).

results = api.collection_search(
    filter = "platform = 'Envisat' and ( instrument = 'MERIS' or instrument = 'MIPAS' ) and organisationName = 'ESA/ESRIN'"
)
data = results.collection_list_as_dict()
df = pd.json_normalize(data, record_path=['collections'])
df[['id', 'title']]

Example: 8.2

CQL filter for granule search with comparison operators. Search granules with cloudCover between 10 and 15%.

from pystac_client import Client 
api = Client.open(URL_LANDING_PAGE)  

results = api.search(
    method = 'GET',   
    max_items = 30,
    collections = [COLLECTION_ID3_CLOUDS],
    filter = "cloudCover >= 10 and cloudCover < 15"   
)
print(f"{results.matched()} items found.")
# Display cloud-cover values as histogram to show that range is taken into account
stac_json = results.item_collection_as_dict()
gdf = gpd.GeoDataFrame.from_features(stac_json)
try:
  _ = gdf[['title','eo:cloud_cover']].hist()
except:
  print("eo:cloud_cover information is not available.")

Further Reading#