STAC is a specification for the handling of geospatial asset catalogs. It has emerged from the efforts of several companies, governmental and non-governmental organizations in the geospatial domain.
In a previous article we gave you an overview of STAC and our new storage capabilities with STAC, explaining the specification and its benefits in a nutshell. In this blog post we are going to further explain why we brought STAC to UP42 storage and introduced STAC-compatible endpoints for all geospatial assets in storage by giving you real-world examples. We will show you how to overcome common data management challenges by customizing assets in storage, searching for a specific asset property or creating a consolidated view of your assets through sort and filter operations. This first article uses a Python HTTP client library, in this case httpx. The way the authentication is done in httpx, via httpx-auth makes it very transparent how the Client Credentials OAuth flow is used to authenticate with the UP42 API.
You can run the code below in console based IPython or in Jupyter lab or classic notebook. For simplicity’s sake we refer to it simply as notebook.
It explores the API entry points and then proceeds towards real use cases, e.g., searching for assets that intersect a given AOI, etc.
mkvirtualenv --python=$(which python3) up42-asset-service-api
workon up42-asset-service-api
pip install jupyterlab
Now we can install the needed modules from inside Jupyter.
!pip install requests
import requests
USERNAME = "your-username"
PASSWORD = "your-password"
WORKSPACE_ID = "your-workspace-id"
def get_auth_token_prod(username, password):
"""Authentication based on UP42 username and password"""
AUTH_URL = "https://api.up42.com/oauth/token"
req_headers = {
"Content-Type": "application/x-www-form-urlencoded",
}
req_body = {
"grant_type": "password",
"username": username,
"password": password}
response = requests.post(AUTH_URL, data=req_body, headers=req_headers)
return response.json()['data']['accessToken']
Let us create a few auxiliary functions that will be useful throughout the notebook.
from IPython.display import JSON, DisplayObject
def up42_asset_stac_url(url: str) -> str:
"""Create a UP42 Spatial Asset service URL."""
return f"https://api.up42.com/v2/assets/stac{url}"
def ppjson(json_data: dict, expand:bool=False) -> DisplayObject:
"""Pretty print JSON data."""
return JSON(json_data, expanded=expand)
Let us naively ask for the available collections.
def get_request(access_token: str, url: str) -> dict:
"""Send a GET request to the UP42 STAC Asset service."""
headers = {
"Authorization": f"Bearer {access_token}"
}
response = requests.get(url, headers=headers)
return response.json()
access_token = get_auth_token_prod(username=USERNAME, password=PASSWORD)
collections = get_request(access_token, url = "https://api.up42.com/v2/assets/stac/collections")
ppjson(collections)
<IPython.core.display.JSON object>
Each collection represents a set of assets. These assets can be
multiple, e.g., a triple of images for a tri-stereo optical satellite
acquisition. The list of collections is paginated, by default 10
items are returned and we get a link with the next
relationship
attribute that tells us how to get the next page of items. In this case:
next(filter(lambda e: e.get("rel") == "next", collections["links"])).get("href")
'https://api.up42.com/v2/assets/stac/collections?token=next:ab984c55-d07b-4aac-992d-44243a77c55c'
But what if we want to have more than 10 items? Is there a way to do it?
The answer is yes. In fact there is a query parameter limit
that has a
default value of 10 that we can set. Let’s ask for the first 20
collections.
access_token = get_auth_token_prod(username=USERNAME, password=PASSWORD)
limit = 20
collections = get_request(access_token, url = "https://api.up42.com/v2/assets/stac/collections"+ f"?limit={limit}")
ppjson(collections)
<IPython.core.display.JSON object>
access_token = get_auth_token_prod(username=USERNAME, password=PASSWORD)
my_collections = get_request(access_token, url = "https://api.up42.com/v2/assets/stac/collections"+ f"?workspace_id={WORKSPACE_ID}")
ppjson(collections)
<IPython.core.display.JSON object>
As you can see now the up42-system:workspace_id
field is scoped to my
workspace, from which I got the first 10 collections. The ordering is by
descending chronological order of the createdAt
field. This field is
part of the STAC
extensions
we defined.
Before we proceed, let us install a package that makes traversing dictionaries easier to do.
%pip install toolz
Requirement already satisfied: toolz in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (0.12.0) Note: you may need to restart the kernel to use updated packages.
from toolz.dicttoolz import get_in
Let us now look inside a given collection. The first item given by the
entry with index 0
of the collections
list is:
ppjson(get_in(["collections", 0], my_collections), expand=True)
<IPython.core.display.JSON object>
We get, among others:
One of the big benefits of using STAC is that by adhering to HATEOAS it allows us to more easily explore an archive.
Inspecting STAC items requires a STAC collection ID. We can either use
the ID field or just use the links
field with the items
relationship
and extract the URL in the href
field.
my_first_coll_url = next(filter(lambda e: e.get("rel") == "items",
get_in(["collections", 1, "links"], my_collections)))["href"]
my_first_coll_url
'https://api.up42.com/v2/assets/stac/collections/bd7454d3-7e39-4c55-9aff-f3dbd362c195/items'
my_first_assets = get_request(access_token, url=my_first_coll_url)
ppjson(my_first_assets, expand=True)
<IPython.core.display.JSON object>
In this case there is only one asset. But it may occur that there are multiple assets inside a collection. For example a pair of assets for stereo, and a triple of assets for a tri-stereo image. For further information on the STAC implementation for the asset service please consult the relevant documentation.
In this case there is only one asset, but it is instructive to see how
you can obtain a specific item from a collection. For that we need the
asset ID, given above as the feature ID, however it is more expedient to
use the URL of the link with the self
relationship given for the first
(and unique, in this case) feature.
my_first_asset_url = next(filter(lambda e: e.get("rel") == "self",
get_in(["features", 0, "links"], my_first_assets)))["href"]
my_first_asset_url
'https://api.up42.com/v2/assets/stac/collections/bd7454d3-7e39-4c55-9aff-f3dbd362c195/items/167125ac-d7f4-44d5-b7a4-7c67c3730826'
my_first_assets = get_request(access_token, url=my_first_coll_url)
ppjson(my_first_assets, expand=True)
<IPython.core.display.JSON object>
One of the most important aspects of the asset STAC service is that it provides a unified interface to search for data. It relies on the STAC Item Search specification.
The UP42 Asset service implements the full featured filtering. It relies on the STAC filter API. Simple search filters and CQL2 based filters are supported.
We can obtain the list of the possible CQL2 based filters with:
queryables = get_request(access_token, url = "https://api.up42.com/v2/assets/stac/queryables")
ppjson(queryables, expand=True)
<IPython.core.display.JSON object>
Let us now search for all data that intersects a given geometry. In this case I want to know of all AOIs that contain a particular Point Of Interest (POI).
from geojson import Point, Polygon, MultiPolygon, FeatureCollection, Feature
my_first_poi = Point((-8.986910071109419,38.46994674758601))
my_first_poi
{"coordinates": [-8.98691, 38.469947], "type": "Point"}
We need now to build a request body, since the search is done with a POST request.
req_body_poi = dict(intersects = my_first_poi)
req_body_poi
{'intersects': {"coordinates": [-8.98691, 38.469947], "type": "Point"}}
Creating a convenience function.
def search_item(req_body: dict, access_token: str):
"""Searches for a STAC item according to the given criteria."""
headers = {
"content-type": "application/json",
"Authorization": f"Bearer {access_token}"
}
return requests.post(
url="https://api.up42.com/v2/assets/stac/search",
headers=headers,
json=req_body,
)
search_results = search_item(my_first_poi, access_token).json()
ppjson(search_results)
<IPython.core.display.JSON object>
Now that we have the assets that intersect the point we have given
my_first_poi
let us draw a map centered on the point and add a marker
placed on it.
First we need to install ipyleaflet.
%pip install ipyleaflet
Requirement already satisfied: ipyleaflet in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (0.17.2) Requirement already satisfied: ipywidgets<9,>=7.6.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipyleaflet) (8.0.4) Requirement already satisfied: traittypes<3,>=0.2.1 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipyleaflet) (0.2.1) Requirement already satisfied: xyzservices>=2021.8.1 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipyleaflet) (2023.2.0) Requirement already satisfied: branca>=0.5.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipyleaflet) (0.6.0) Requirement already satisfied: jinja2 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from branca>=0.5.0->ipyleaflet) (3.1.2) Requirement already satisfied: ipykernel>=4.5.1 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipywidgets<9,>=7.6.0->ipyleaflet) (6.21.2) Requirement already satisfied: ipython>=6.1.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipywidgets<9,>=7.6.0->ipyleaflet) (8.10.0) Requirement already satisfied: traitlets>=4.3.1 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipywidgets<9,>=7.6.0->ipyleaflet) (5.9.0) Requirement already satisfied: widgetsnbextension~=4.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipywidgets<9,>=7.6.0->ipyleaflet) (4.0.5) Requirement already satisfied: jupyterlab-widgets~=3.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipywidgets<9,>=7.6.0->ipyleaflet) (3.0.5) Requirement already satisfied: appnope in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (0.1.3) Requirement already satisfied: comm>=0.1.1 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (0.1.2) Requirement already satisfied: debugpy>=1.6.5 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (1.6.6) Requirement already satisfied: jupyter-client>=6.1.12 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (8.0.3) Requirement already satisfied: jupyter-core!=5.0.*,>=4.12 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (5.2.0) Requirement already satisfied: matplotlib-inline>=0.1 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (0.1.6) Requirement already satisfied: nest-asyncio in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (1.5.6) Requirement already satisfied: packaging in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (23.0) Requirement already satisfied: psutil in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (5.9.4) Requirement already satisfied: pyzmq>=20 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (25.0.0) Requirement already satisfied: tornado>=6.1 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (6.2) Requirement already satisfied: backcall in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (0.2.0) Requirement already satisfied: decorator in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (5.1.1) Requirement already satisfied: jedi>=0.16 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (0.18.2) Requirement already satisfied: pickleshare in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (0.7.5) Requirement already satisfied: prompt-toolkit<3.1.0,>=3.0.30 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (3.0.36) Requirement already satisfied: pygments>=2.4.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (2.14.0) Requirement already satisfied: stack-data in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (0.6.2) Requirement already satisfied: pexpect>4.3 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (4.8.0) Requirement already satisfied: MarkupSafe>=2.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from jinja2->branca>=0.5.0->ipyleaflet) (2.1.2) Requirement already satisfied: parso<0.9.0,>=0.8.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from jedi>=0.16->ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (0.8.3) Requirement already satisfied: python-dateutil>=2.8.2 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from jupyter-client>=6.1.12->ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (2.8.2) Requirement already satisfied: platformdirs>=2.5 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from jupyter-core!=5.0.*,>=4.12->ipykernel>=4.5.1->ipywidgets<9,>=7.6.0->ipyleaflet) (3.0.0) Requirement already satisfied: ptyprocess>=0.5 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from pexpect>4.3->ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (0.7.0) Requirement already satisfied: wcwidth in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from prompt-toolkit<3.1.0,>=3.0.30->ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (0.2.6) Requirement already satisfied: executing>=1.2.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (1.2.0) Requirement already satisfied: asttokens>=2.1.0 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (2.2.1) Requirement already satisfied: pure-eval in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from stack-data->ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (0.2.2) Requirement already satisfied: six in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from asttokens>=2.1.0->stack-data->ipython>=6.1.0->ipywidgets<9,>=7.6.0->ipyleaflet) (1.16.0) Note: you may need to restart the kernel to use updated packages.
Now importing the needed methods/functions from that module.
from ipyleaflet import Map, GeoJSON, Marker, AwesomeIcon
We need to define a center for the map. The center is given in EPSG:4326 (WGS84 datum) as (latitude, longitude). In the definition above of POI we have considered a cartesian coordinate order, where longitude corresponds to $x$ and latitude to $y$. So they need to be reversed: $(x, y) \rightarrow (y, x)$. It also needs to be in the form of a tuple.
my_first_map_center = tuple(my_first_poi["coordinates"][::-1])
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[13], line 1 ----> 1 my_first_map_center = tuple(my_first_poi["coordinates"][::-1]) NameError: name 'my_first_poi' is not defined
mymap = Map(center=my_first_map_center, zoom=14)
# Add the AOI to the map. First style it and then add it.
aoi_layer = GeoJSON(
data=search_results,
style={"opacity": 1, "dashArray": "9", "fillOpacity": 0.5, "weight": 1},
hover_style={"color": "yellow", "dashArray": "0", "fillOpacity": 0.5},
)
# Add a marker layer at the center.
marker_layer = Marker(location=my_first_map_center,
draggable=False,
icon=AwesomeIcon(name="close",
color_marker="green"))
mymap.add_layer(aoi_layer)
mymap.add_layer(marker_layer)
mymap
Map(center=[38.469947, -8.98691], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', …
We can do complex searches using the Common Query Language version 2 (CQL2) as defined in the STAC specification for full-featured search of STAC items.
To illustrate this we are going to perform a search for all assets that intersect a given geometry and we want those with a cloud cover below 10%. Of course that the latter only makes sense for optical imagery.
We need to build the query. It is composed of two parts:
intersects
field set to the geometry we are using to compute
the intersections.We are going to read a GeoJSON file with the geometry.
path2geom = "../examples/portugal_envelope.geojson"
import json
with open(path2geom, "r") as f:
geom_map = json.load(f)
We want to draw a map of the geometry. We need to find the center. We can use shapely for that.
%pip install shapely
Requirement already satisfied: shapely in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (2.0.1) Requirement already satisfied: numpy>=1.14 in /Users/appa/.virtualenvs/httpx-notebooks/lib/python3.11/site-packages (from shapely) (1.24.2) Note: you may need to restart the kernel to use updated packages.
from shapely.geometry import shape, mapping
We need to extract the geonetry from the collection. Let us see first how many features there are.
len(get_in(["features"], geom_map))
1
Only one feature. So to extract the geometry we need only to extract the
value for the key ["features"][0]["geometry"]
.
shape_geom_map = shape(get_in(["features", 0, "geometry"], geom_map))
shape_geom_map
The centroid is given by:
intersect_map_center = shape_geom_map.centroid
print(intersect_map_center)
POINT (-8.115254781697322 39.67809052275443)
Let us draw the map.
intersect_map = Map(center=(intersect_map_center.y, intersect_map_center.x), zoom=6)
# Add the AOI to the map. First style it and then add it.
intersect_layer = GeoJSON(
data=geom_map,
style={"opacity": 1, "dashArray": "9", "fillOpacity": 0.5, "weight": 1},
hover_style={"color": "yellow", "dashArray": "0", "fillOpacity": 0.5},
)
# Add a marker layer at the center.
intersect_marker_layer = Marker(location=(intersect_map_center.y, intersect_map_center.x),
draggable=False, icon=AwesomeIcon(name="close",
color_marker="green"))
intersect_map.add_layer(intersect_layer)
intersect_map.add_layer(intersect_marker_layer)
intersect_map
Map(center=[39.67809052275443, -8.115254781697322], controls=(ZoomControl(options=['position', 'zoom_in_text',…
This is the geometry for which we want to look for intersecting assets. We can start to build the request body dictionary. We need to get the shapely geometry to be a simple dictionary and not a shapely type.
type(shape_geom_map)
shapely.geometry.polygon.Polygon
intersect_req_body = dict(intersects = mapping(shape_geom_map))
intersect_req_body
{'intersects': {'type': 'Polygon', 'coordinates': (((-9.52171458613654, 36.853921776429516), (-7.1631974367996065, 36.82161569679076), (-5.996066469639629, 42.04883107989275), (-9.841226786721336, 42.14603469432703), (-9.52171458613654, 36.853921776429516)),)}}
Now is the time to add the CQL2 filter. This is always something of the
form: <filter> <operator> <args>
. The arguments can be composed of
other operators and args. In our case we want to limit all results to be
with a cloud cover less than 10%. In its simplest form the arguments are
logical clauses that must be satisfied. These clauses can be represented
in JSON as an array of operands and operators. In this case, concretely:
my_cloud_cover_dict = dict(filter=dict(args=[
dict(property="eo:cloud_cover"), 10],
op="<"))
my_cloud_cover_dict
{'filter': {'args': [{'property': 'eo:cloud_cover'}, 10], 'op': '<'}}
The structure of CQL2 is basically a syntax
tree with the nodes
being the operators and the root being the node filter
.
Now we can update the request body with this filter.
new_intersect_req_body = intersect_req_body | my_cloud_cover_dict
new_intersect_req_body
{'intersects': {'type': 'Polygon', 'coordinates': (((-9.52171458613654, 36.853921776429516), (-7.1631974367996065, 36.82161569679076), (-5.996066469639629, 42.04883107989275), (-9.841226786721336, 42.14603469432703), (-9.52171458613654, 36.853921776429516)),)}, 'filter': {'args': [{'property': 'eo:cloud_cover'}, 10], 'op': '<'}}
We can now perform the search:
complex_search_results = search_item(new_intersect_req_body, access_token).json()
ppjson(complex_search_results)
<IPython.core.display.JSON object>
We have more than 10 results, since I want to have only the ones in my workspace I need to add my workspace as a CQL2 filter to the request.
cloud_cover_workspace_dict = dict(filter=
dict(args=[
dict(args=[dict(property="eo:cloud_cover"), 10],
op="<"),
dict(args=[dict(property="workspace_id"), WORKSPACE_ID],
op="=")],
op="and")
)
cloud_cover_workspace_dict
{'filter': {'args': [{'args': [{'property': 'eo:cloud_cover'}, 10], 'op': '<'}, {'args': [{'property': 'workspace_id'}, 'd39fe05a-400c-44f6-b770-86990f64b004'], 'op': '='}], 'op': 'and'}}
Performing the search with the two filters (cloud cover and workspace ID):
complex_search_results = search_item(intersect_req_body | cloud_cover_workspace_dict, access_token).json()
ppjson(complex_search_results)
<IPython.core.display.JSON object>
We have 7 results. Let us try to visualize the returned items on the map. We see that this is exactly the AOI we have above in @map-collection-point.
complex_search_results_layer = GeoJSON(data=complex_search_results,
style={"color":"red", "opacity": 1,
"dashArray": "9", "fillOpacity": 0.5,
"weight": 1},
hover_style={"color": "white", "dashArray": "0", "fillOpacity": 0.5},
)
intersect_map.add_layer(complex_search_results_layer)
intersect_map
Map(center=[39.67809052275443, -8.115254781697322], controls=(ZoomControl(options=['position', 'zoom_in_text',…
The features returned are too small to be easily detected without panning & zooming. To make it clearer we are going to recenter the map on the point of interest we introduced above and display only the returned features.
complex_search_results_map = Map(center=my_first_map_center, zoom=12)
complex_search_results_map.add_layer(complex_search_results_layer)
complex_search_results_map
Map(center=[38.469947, -8.98691], controls=(ZoomControl(options=['position', 'zoom_in_text', 'zoom_in_title', …
We can see clearly the returned features in this area.
We can easily extract the assets ID for downloading.
complex_search_asset_ids = list(map(lambda e: get_in(["properties", "up42-system:asset_id"], e),
complex_search_results["features"]))
complex_search_asset_ids
['630bd8cd-c4c2-4619-9a88-5b3421ffd1de', '828b2da8-b322-4e19-9abc-3917e0e36986', 'f5080d5d-cb90-4cb5-9caf-b1e870865967', 'fa438b5e-0903-4787-8958-45a376dd9b04', '666191ca-0f28-4f49-9bae-597865187eb1', '696f6b22-523a-4f57-a6a3-16efa5b1edb5', '55dfa9f4-361f-44dd-b4c4-f1f05ad02cd3']
Let us create a convenience function for downloading the assets.
def download_asset(asset_id: str, access_token: str) -> None:
"""Downloads an asset with the given ID."""
with open(f"asset_{asset_id}.zip", 'wb') as output:
headers = {"Authorization": f"Bearer {access_token}"}
response = requests.get(f"https://api.up42.com/v2/assets/{asset_id}", headers=headers, stream=True)
for data in response.iter_content(chunk_size=8192):
output.write(data)
We can iterate on the list of asset IDs.
gen_assets = (download_asset(id) for id in complex_search_asset_ids)
Uncomment the line below to download all assets listed above.
# list(gen_assets)
STAC offers a very efficient and natural way to search catalogs of geospatial assets. The current implementation on top of the UP42 storage provides a large degree of flexibility, making it easy to access assets using a multitude of search options. All assets are provided as hypermedia, making it very simple to manipulate.
Go ahead and try the UP42 Spatial Asset Service API.