#!/usr/bin/env python3 # vim: set fileencoding=utf-8 # pylint: disable=C0103 """ Search of Sentinel images. Copyright (C) 2016-18, Carlo de Franchis <carlo.de-franchis@ens-cachan.fr> This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. """ import os import sys import argparse import datetime import json import shapely.geometry import shapely.wkt import requests import dateutil.parser from tsd import utils # http://sentinel-s2-l1c.s3-website.eu-central-1.amazonaws.com API_URLS = { 'copernicus': 'https://scihub.copernicus.eu/dhus/', 'austria': 'https://data.sentinel.zamg.ac.at/', 'finland': 'https://finhub.nsdc.fmi.fi/', 's5phub': 'https://s5phub.copernicus.eu/dhus/' } def read_copernicus_credentials_from_environment_variables(): """ Read the user Copernicus Open Access Hub credentials. """ try: login = os.environ['COPERNICUS_LOGIN'] password = os.environ['COPERNICUS_PASSWORD'] except KeyError as e: login = 'tsd-default-user' password = 'b3c5e714034282ea5c' return login, password def post_scihub(url, query, user, password): """ Send a POST request to scihub. """ r = requests.post(url, dict(q=query), auth=(user, password)) if r.ok: return r else: print('ERROR:', end=' ') if r.status_code == 503: print('The Sentinels Scientific Data Hub is down. Check on' ' https://scihub.copernicus.eu/dhus/#/home') elif r.status_code == 401: print('Authentication failed with', user, password) else: print('Scientific Data Hub returned error', r.status_code) r.raise_for_status() def build_scihub_query(aoi, start_date=None, end_date=None, satellite='Sentinel-1', product_type='GRD', operational_mode='IW', relative_orbit_number=None, swath_identifier=None, search_type='contains'): """ Args: search_type (str): either "contains" or "intersects" """ # default start/end dates if end_date is None: end_date = datetime.datetime.now() if start_date is None: start_date = datetime.datetime(2000, 1, 1) # build the url used to query the scihub API query = 'platformname:{}'.format(satellite) query += ' AND producttype:{}'.format(product_type) if satellite == 'Sentinel-1': query += ' AND sensoroperationalmode:{}'.format(operational_mode) query += ' AND beginposition:[{}Z TO {}Z]'.format(start_date.isoformat(), end_date.isoformat()) if relative_orbit_number is not None: query += ' AND relativeorbitnumber:{}'.format(relative_orbit_number) if swath_identifier is not None: query += ' AND swathidentifier:{}'.format(swath_identifier) # queried polygon or point # http://forum.step.esa.int/t/advanced-search-in-data-hub-contains-intersects/1150/2 query += ' AND footprint:\"{}({})\"'.format(search_type, shapely.geometry.shape(aoi).wkt) return query def load_query(query, api_url, credentials, start_row=0, page_size=100): """ Do a full-text query on the SciHub API using the OpenSearch format. https://scihub.copernicus.eu/twiki/do/view/SciHubUserGuide/3FullTextSearch """ # load query results url = '{}search?format=json&rows={}&start={}'.format(api_url, page_size, start_row) login, password = credentials r = post_scihub(url, query, login, password) # parse response content d = r.json()['feed'] total_results = int(d['opensearch:totalResults']) entries = d.get('entry', []) # if the query returns only one product entries will be a dict, not a list if isinstance(entries, dict): entries = [entries] # repeat query until all results have been loaded output = entries if total_results > start_row + page_size: output += load_query(query, api_url, credentials, start_row=(start_row + page_size)) return output def prettify_scihub_dict(d): """ Convert the oddly formatted json response of scihub into something nicer. Scihub json response is roughly a dict with a key per datatype (int, str, date, ...). The value associated to each key is a list of dicts like {'name': foo, 'content': bar}, while it would be more natural to have (foo, bar) as a (key, value) pair of the main json dict. Args: d (dict): json-formatted metadata returned by scihub for a single SAFE Returns: prettified metadata dict """ out = {} for k in ['title', 'id', 'summary']: if k in d: out[k] = d[k] if 'int' in d: if isinstance(d['int'], list): for x in d['int']: out[x['name']] = int(x['content']) else: x = d['int'] out[x['name']] = int(x['content']) if 'str' in d: for x in d['str']: out[x['name']] = x['content'] if 'date' in d: for x in d['date']: out[x['name']] = x['content'] if 'link' in d: out['links'] = {} for x in d['link']: if 'rel' in x: out['links'][x['rel']] = x['href'] else: out['links']['main'] = x['href'] return out def search(aoi, start_date=None, end_date=None, satellite='Sentinel-1', product_type='GRD', operational_mode='IW', relative_orbit_number=None, swath_identifier=None, api='copernicus', search_type='contains'): """ List the Sentinel images covering a location using Copernicus Scihub API. """ if satellite == 'Sentinel-2' and product_type not in ['S2MSI1C', 'S2MSI2A', 'S2MSI2Ap']: product_type = 'S2MSI1C' if satellite == 'Sentinel-5P': satellite = 'Sentinel-5' if satellite == 'Sentinel-5': api = 's5phub' query = build_scihub_query(aoi, start_date, end_date, satellite, product_type, operational_mode, relative_orbit_number, swath_identifier, search_type=search_type) if api == "s5phub": creds = ("s5pguest", "s5pguest") else: creds = read_copernicus_credentials_from_environment_variables() results = [prettify_scihub_dict(x) for x in load_query(query, API_URLS[api], creds)] # check if the image footprint contains the area of interest not_covering = [] aoi_shape = shapely.geometry.shape(aoi) for x in results: if not shapely.wkt.loads(x['footprint']).contains(aoi_shape): not_covering.append(x) for x in not_covering: results.remove(x) return results if __name__ == '__main__': parser = argparse.ArgumentParser(description=('Search of Sentinel images')) parser.add_argument('--geom', type=utils.valid_geojson, help=('path to geojson file')) parser.add_argument('--lat', type=utils.valid_lat, help=('latitude of the center of the rectangle AOI')) parser.add_argument('--lon', type=utils.valid_lon, help=('longitude of the center of the rectangle AOI')) parser.add_argument('-w', '--width', type=int, default=5000, help='width of the AOI (m), default 5000 m') parser.add_argument('-l', '--height', type=int, default=5000, help='height of the AOI (m), default 5000 m') parser.add_argument('-s', '--start-date', type=utils.valid_datetime, help='start date, YYYY-MM-DD') parser.add_argument('-e', '--end-date', type=utils.valid_datetime, help='end date, YYYY-MM-DD') parser.add_argument('--satellite', default='Sentinel-1', help='which satellite: Sentinel-1 or Sentinel-2') parser.add_argument('--product-type', default='GRD', help='type of image: RAW, SLC, GRD, OCN (for S1), S2MSI1C, S2MSI2A, S2MSI2Ap (for S2)') parser.add_argument('--operational-mode', default='IW', help='(for S1) acquisiton mode: SM, IW, EW or WV') parser.add_argument('--swath-identifier', help='(for S1) subswath id: S1..S6 or IW1..IW3 or EW1..EW5') parser.add_argument('--api', default='copernicus', help='mirror to use: copernicus, austria or finland') args = parser.parse_args() if args.geom and (args.lat or args.lon): parser.error('--geom and {--lat, --lon} are mutually exclusive') if not args.geom and (not args.lat or not args.lon): parser.error('either --geom or {--lat, --lon} must be defined') if args.geom: aoi = args.geom else: aoi = utils.geojson_geometry_object(args.lat, args.lon, args.width, args.height) print(json.dumps(search(aoi, args.start_date, args.end_date, satellite=args.satellite, product_type=args.product_type, operational_mode=args.operational_mode, swath_identifier=args.swath_identifier, api=args.api)))