Thank you for the recommendation, @avrecko. I wound up implementing a process that uses the WebFeatureService
function from the python API to query for all dates for a given bounding box then parses the scene IDs (granule naming convention) to filter down to the desired relative orbits. I can add the list of exact dates that I want to include to my evalscript so only images from the desired relative orbits make it past filterScenes
.
Here are the python functions that I wrote to get the job done:
from sentinelhub import BBox, bbox_to_dimensions, CRS, DataCollection, \
Geometry, MimeType, SentinelHubBatch, SentinelHubRequest, SHConfig, \
WebFeatureService
# absolute orbits translate to relative orbits differently for 2A and 2B
# I arrived at this solution after some trial and error...
def absolute_to_relative_orbit(x, sat):
assert sat in ['2A', '2B']
if sat == '2A':
adj = -140
if sat == '2B':
adj = -26
return (x + adj) % 143
def get_dates_by_orbit(bbox, start_date, end_date, max_cc, target_orbit, config):
assert target_orbit is not None, "relative_orbit must be specified"
# convert target_orbit to list if just a single orbit
if type(target_orbit) is int:
target_orbit = [target_orbit]
# define time window
search_time_interval = (f'{start_date}T00:00:00', f'{end_date}T23:59:59')
# query scenes
wfs_iterator = WebFeatureService(
bbox,
search_time_interval,
data_collection=DataCollection.SENTINEL2_L2A,
maxcc=max_cc,
config=config
)
# filter down to dates from specified orbit(s)
dates = []
for tile_info in wfs_iterator:
# raw product ID
id = tile_info['properties']['id']
# acquisition date
date = tile_info['properties']['date']
# absolute orbit is buried in ID after _A string
absolute_orbit = int(re.search("(?<=_A)[\d]+(?=_)", id).group())
# satellite (either 2A or 2B) is defined at beginning of id
sat = id[1:3]
# convert to relative orbit
relative_orbit = absolute_to_relative_orbit(absolute_orbit, sat)
if relative_orbit not in target_orbit:
continue
# add date if not already added to list
if date not in dates:
dates.append(date)
assert len(dates) > 0, \
f'No dates available for this bounding box and relative orbit {target_orbit}'
return dates
def filter_dates(dates, months, years):
# convert date strings to date objects
dates = [dt.datetime.strptime(date, '%Y-%m-%d').date() for date in dates]
# filter down to supplied months/years
filtered = [str(date) for date in dates if date.month in months and date.year in years]
assert len(filtered) > 0, \
'None of supplied dates satisfy desired months/years'
return filtered
The logic in the absolute to relative orbit function seems fragile but it has worked in my tests so far.
Assuming you have a Geometry
object called geom
, a config
object with credentials, and a evalscript
string that is waiting for a string of dates to pass to filterScenes
, you can implement the orbit-wise filter like this:
dates = get_dates_by_orbit(
geom.bbox,
start_date='2019-06-01',
end_date='2020-09-01',
max_cc=0.5,
target_orbit = [126, 83, 40], # can be one or multiple orbits
config=config)
# filter down to summer only
dates_filt = filter_dates(dates, months=[6, 7, 8], years=[2019, 2020])
# make list of dates into string to pass to evalscript
date_string = ', '.join([f'"{date}"' for date in dates_filt])
eval_formatted = evalscript % date_string
eval_formatted
has the explicit list of dates in the filterScenes
function and can be passed to SentinelHubRequest
My evalscript is stored as a python string, the filterScenes
function looks like this:
'''
function filterScenes(availableScenes, inputMetadata) {
var allowedDates = [%s]; // format with python
return availableScenes.filter(function (scene) {
var sceneDateStr = scene.date.toISOString().split("T")[0]; //converting date and time to string and rounding to day precision
return allowedDates.includes(sceneDateStr);
});
}
'''