Gridded Search Tutorial
The Discovery API provides an imagegrid service that searches the catalog in much the same way as the standard STAC service, but returns images gridded into tiles. Tiles are 5 km by 5 km in size in UTM coordinates.
The base URL of the service is https://api.maxar.com/discovery/v1/services/imagegrid
and it implements two methods:
- imagegrid/search. Search using various query parameters and return tiles.
- imagegrid/items/{itemId}. Query a single tile of an image.
Only the following Maxar imagery collections are searched by the imagegrid service:
- wv01
- wv02
- wv03-vnir
- wv04
- ge01
- lg01
- lg02
This example uses Python to demonstrate queries on the imagegrid service. The requests package is used for sending queries to the service and ipyleaflet for mapping the responses.
First the required Python imports:
import json
from getpass import getpass
import requests
from ipyleaflet import Map, GeoJSON, basemaps, basemap_to_tiles
An MGP OAuth2 bearer token or an API key is required for authorization. Generate a token and enter it below. An HTTP Authorization header will be passed with every request to the API.
To use an OAuth2 bearer token:
token = getpass('Token: ')
headers = {
'Authorization': f'Bearer {token}'
}
discovery_url = 'https://api.maxar.com/discovery/v1'
Token: ········
To use an API key:
token = getpass('Token: ')
headers = {
'maxar-api-key': key
}
discovery_url = 'https://api.maxar.com/discovery/v1'
Token: ········
Tiling a single image
Requests to the imagegrid service can result in a large number of tiles being returned. So we begin with a simple example of querying a single image, the WorldView-1 image 10203C0004ECF600. First we query the image using the Discovery API in the usual way without tiling the response:
item_id = '10203C0004ECF600'
headers
response = requests.get(f'{discovery_url}/search', headers=headers, params={'ids': item_id})
feature_coll = json.loads(response.text)
feature = feature_coll['features'][0]
View the image's footprint using ipyleaflet:
center = [38.654, -104.907]
zoom = 9
map1 = Map(basemap=basemaps.OpenStreetMap.Mapnik, zoom=zoom, center=center, zoom_control=False)
map1.add(GeoJSON(data=feature))
map1
Now we will query the same image but perform gridding using the service at the URL path /services/imagegrid. The search method at this endpoint accepts most of the same query parameters as the Catalog search. In this case we again use the ids
query parameter:
response = requests.get(f'{discovery_url}/services/imagegrid/search',
headers=headers, params={'ids': item_id})
feature_coll = json.loads(response.text)
features = feature_coll['features']
print(f'Number of returned tiles: {len(features)}')
Number of returned tiles: 62
map2 = Map(basemap=basemaps.OpenStreetMap.Mapnik, zoom=zoom, center=center, zoom_control=False)
map2.add(GeoJSON(data=feature_coll))
map2
Tile properties
Each returned feature in the feature collection is a single tile. Here is what one looks like:
feature = features[0]
feature['id']
'Z13-031131311132-10203C0004ECF600'
Tile feature IDs consist of three parts: UTM zone, quadkey, and image identifier. The UTM zone and quadkey together uniquely identify the tile's cell while the image identifier is the tile's image.
Here are the tile's links:
feature['links']
[{'rel': 'self',
'href': 'https://api.maxar.com/discovery/v1/services/imagegrid/items/Z13-031131311132-10203C0004ECF600',
'type': 'application/geo+json'},
{'rel': 'parent',
'href': 'https://api.maxar.com/discovery/v1/collections/wv01/items/10203C0004ECF600',
'type': 'application/geo+json'}]
The self link uses the imagegrid service's items
method to point to this single tile. Any gridded tile returned by the imagegrid service can be referenced this way.
The parent link points to the Discovery API's item for the tile's image.
Both of these links can be queried using a GET request and the same OAuth2 token used with the imagegrid service.
Here are the tile's properties:
feature['properties']
{'cell_id': 'Z13-031131311132',
'acquisition_id': '10203C0004ECF600',
'tile:data_percentage': 23,
'tile:no_data_percentage': 77,
'tile:quadkey': '031131311132',
'tile:zone': 13,
'aoi:data_area_sqkm': 5.682,
'aoi:data_percentage': 23,
'constellation': 'maxar',
'datetime': '2023-03-01T20:48:24.196113Z',
'eo:cloud_cover': 96.33499928178921,
'instruments': ['VNIR'],
'multi_resolution_avg': None,
'multi_resolution_max': None,
'multi_resolution_min': None,
'off_nadir_max': 25.819397,
'off_nadir_min': 22.896294,
'pan_resolution_avg': 0.5884959283333333,
'pan_resolution_max': 0.6015993,
'pan_resolution_min': 0.5783442,
'platform': 'worldview-01',
'view:azimuth': 344.4453383333333,
'view:off_nadir': 24.1979035,
'view:sun_azimuth': 211.40923833333332,
'view:sun_elevation': 38.925265333333336}
A few of these properties are specific to the imagegrid service while the remainder are copied from the tile's image. These are the properties added by the imagegrid service:
Property | Description |
---|---|
cell_id | UTM zone and quadkey |
acquisition_id | Image identifier (or item ID) |
tile:data_percentage | Percent of cell covered by image |
tile:no_data_percentage | 100 - tile:data_percentage |
tile:quadkey | quadkey |
tile:zone | UTM zone |
aoi:data_area_sqkm | Area of cell intersected with image and AOI |
aoi:data_percentage | Percent of cell covered by image and AOI |
The two "aoi" properties take into account any area of interest specified in the search using the "intersects" or "bbox" query parameters.
The remaining properties are copied from those of the tile's image. Not all of the image properties are included -- to see them all query the self
link as described above.
Paging
Searches on the imagegrid service will commonly return a large number of tiles and paging will be required to read them all. By default the imagegrid/search method will return up to 100 tiles in a response but the "limit" query parameter can be specified to return up to 1000.
The imagegrid method always sorts tiles in the response by (datetime DESC, id ASC). The "sortby" query parameter that the STAC API uses is not supported.
Here is a query using a bounding box and time range that returns tiles from a number of images. Only tiles that intersect the bounding box are returned.
params = {
'bbox': '-105,38.5,-104.5,39',
'datetime': '2020-01-01T00:00:00Z/2022-03-15T00:00:00Z'
}
response = requests.get(f'{discovery_url}/services/imagegrid/search', headers=headers, params=params)
feature_coll1 = json.loads(response.text)
features = feature_coll1['features']
print(f'Number of returned tiles: {len(features)}')
Number of returned tiles: 100
map3 = Map(basemap=basemaps.OpenStreetMap.Mapnik, zoom=zoom, center=center, zoom_control=False)
map3.add(GeoJSON(data=feature_coll1))
map3
The response includes a "next" link that can be used to request the next page of results:
feature_coll1['links']
[{'rel': 'next',
'href': 'https://api.maxar.com/discovery/v1/services/imagegrid/search?bbox=-105%2C38.5%2C-104.5%2C39&datetime=2020-01-01T00%3A00%3A00Z%2F2022-03-15T00%3A00%3A00Z&limit=100&last=Z13-120020202031-1050010029090A00'}]
The "next" URL contains the same query parameters as the request for the first page (possibly URL-encoded). The "limit" query parameter is also included if the request didn't specify it. But an additional query parameter "last" is also included with the tile ID of the last tile returned in the previous page. This way the imagegrid method knows where to begin the next page of results.
Here we call the "next" link to request the second page of results:
response = requests.get(feature_coll1['links'][0]['href'], headers=headers)
feature_coll2 = json.loads(response.text)
features = feature_coll2['features']
print(f'Number of returned tiles: {len(features)}')
Number of returned tiles: 100
map4 = Map(basemap=basemaps.OpenStreetMap.Mapnik, zoom=zoom, center=center, zoom_control=False)
map4.add(GeoJSON(data=feature_coll2))
map4
Here is some code to fetch all tiles from the query above by repeatedly calling the "next" link in the responses. Paging stops when the response does not have a "next" link. We specify a limit of 1000 tiles per response so that fewer requests are made.
params = {
'bbox': '-105,38.5,-104.5,39',
'datetime': '2022-01-01T00:00:00Z/2023-03-15T00:00:00Z',
'limit': 1000
}
url = f'{discovery_url}/services/imagegrid/search?' + '&'.join(f'{k}={v}' for k,v in params.items())
all_features = []
count = 0
while True:
count += 1
print(f'Reading page {count}: {url}')
response = requests.get(url, headers=headers)
page = json.loads(response.text)
features = page['features']
print(f'Read {len(features)} tiles')
all_features += features
urls = [link['href'] for link in page.get('links', []) if link['rel'] == 'next']
if not urls:
break
url = urls[0]
feature_coll = {
'type': 'FeatureCollection',
'features': all_features
}
Reading page 1: https://api.maxar.com/discovery/v1/services/imagegrid/search?bbox=-105,38.5,-104.5,39&datetime=2022-01-01T00:00:00Z/2023-03-15T00:00:00Z&limit=1000
Read 1000 tiles
Reading page 2: https://api.maxar.com/discovery/v1/services/imagegrid/search?bbox=-105%2C38.5%2C-104.5%2C39&datetime=2022-01-01T00%3A00%3A00Z%2F2023-03-15T00%3A00%3A00Z&limit=1000&last=Z13-120020200012-10300100D2CD7E00
Read 642 tiles
Calculate the number of images involved among all returned tiles:
image_ids = {feature['properties']['acquisition_id'] for feature in feature_coll['features']}
print(f'Number of images: {len(image_ids)}')
Number of images: 47
Here is a map of all returned tiles. Often there will be a large amount of overlap among tiles returned by the imagegrid method. Additional analysis will need to be done to determine which tiles are of interest to the user.
map5 = Map(basemap=basemaps.OpenStreetMap.Mapnik, zoom=zoom, center=center, zoom_control=False)
map5.add(GeoJSON(data=feature_coll))
map5