Add a REST API
Last Updated: July 2024
In this tutorial you will add a REST API endpoint to the Earth Engine app. The REST API will provide a programmatic access point to the underlying get_time_series_from_image_collection
method. This is the same method that is used retrieve the time series for the plot at an area of interest capability of the Viewer page. Topics covered in this tutorial include:
REST API in Tethys Apps
Django REST Framework in Tethys
Token Authentication
Controllers in Multiple Files
Developing REST APIs with Postman
0. Start From Previous Solution (Optional)
If you wish to use the previous solution as a starting point:
git clone https://github.com/tethysplatform/tethysapp-earth_engine.git cd tethysapp-earth_engine git checkout -b clip-by-asset-solution clip-by-asset-solution-0.1
1. Install dependencies
The REST API capability requires djangorestframework
to be installed. Install it using conda or pip as follows:
# conda: conda-forge channel strongly recommended
conda install -c conda-forge djangorestframework
# pip
pip install djangorestframework
2. Add dependencies to install.yml
Add djangorestframework
to the install.yml
file to ensure it is installed when your app is installed as follows:
# This file should be committed to your app code.
version: 1.0
# This should be greater or equal to your tethys-platform in your environment
tethys_version: ">=4.0.0"
# This should match the app - package name in your setup.py
name: earth_engine
requirements:
# Putting in a skip true param will skip the entire section. Ignoring the option will assume it be set to False
skip: false
conda:
channels:
- conda-forge
packages:
- earthengine-api
- oauth2client
- geojson
- pandas
- pyshp
- simplejson
- djangorestframework
pip:
npm:
post:
3. Reorganize Controller Functions into Separate Files
The controllers.py
file is beginning to get quite long. To make the controller code more manageable, in this step you will refactor the controllers into several files.
Create a new folder called
controllers
in theearth_engine
directory with the following new empty Python modules in it:
controllers/
__init__.py
home.py
viewer.py
rest.py
Note
A folder with a file named __init__.py
is called a Python package.
Warning
If you are using PyCharm, make sure it doesn't add an __init__.py
file in the tethysapp
directory. This will cause this app or other apps to stop working when installed in Tethys Platform.
Copy the
home
andabout
controller functions, with any imports they need into the newcontrollers/home.py
module:
import logging
from tethys_sdk.routing import controller
from ..app import App
log = logging.getLogger(f'tethys.apps.{__name__}')
@controller
def home(request):
"""
Controller for the app home page.
"""
context = {}
return App.render(request, 'home.html', context)
@controller
def about(request):
"""
Controller for the app about page.
"""
context = {}
return App.render(request, 'about.html', context)
Copy the
viewer
,get_image_collection
, andget_time_series_plot
controller functions with any imports they need into the newcontrollers/viewer.py
module:
import datetime as dt
import geojson
import logging
from simplejson.errors import JSONDecodeError
from django.http import JsonResponse, HttpResponseNotAllowed, HttpResponseRedirect
from tethys_sdk.routing import controller
from tethys_sdk.gizmos import SelectInput, DatePicker, Button, MapView, MVView, PlotlyView, MVDraw
from ..app import App
from ..helpers import generate_figure, handle_shapefile_upload
from ..gee.methods import get_image_collection_asset, get_time_series_from_image_collection, \
get_boundary_fc_props_for_user
from ..gee.products import EE_PRODUCTS
log = logging.getLogger(f'tethys.apps.{__name__}')
@controller(user_media=True, url='viewer')
def viewer(request, user_media):
"""
Controller for the app viewer page.
"""
... # Code not shown for brevity
return App.render(request, 'viewer.html', context)
@controller(url='viewer/get-image-collection')
def get_image_collection(request):
"""
Controller to handle image collection requests.
"""
... # Code not shown for brevity
return JsonResponse(response_data)
@controller(url='viewer/get-time-series-plot')
def get_time_series_plot(request):
... # Code not shown for brevity
return App.render(request, 'plot.html', context)
Delete the old
controllers.py
file.Navigate to http://localhost:8000/apps/earth-engine/ and verify that the app functions as it did before the change.
4. Create New Controller for REST API Endpoint
REST endpoints are similar to normal controllers. The primary difference is that they typically return data using JSON or XML format instead of HTML. In this step you will create a new controller function for the REST endpoint.
Create a new controller function named
get_time_series
incontrollers/rest.py
with the following contents:
import logging
from django.http import JsonResponse
from tethys_sdk.routing import controller
from rest_framework.authentication import TokenAuthentication
from rest_framework.decorators import api_view, authentication_classes
log = logging.getLogger(f'tethys.apps.{__name__}')
@controller(url='api/get-time-series', login_required=False)
@api_view(['GET', 'POST'])
@authentication_classes((TokenAuthentication,))
def get_time_series(request):
"""
Controller for the get-time-series REST endpoint.
"""
response_data = {
"detail": "Hello, World!"
}
return JsonResponse(response_data)
Tip
Tethys includes the Django REST Framework to aid with the token authentication capability (i.e. api_view()
and authentication_classes()
decorators). It is quite a capable extension for Django websites and is worth investigating if you plan to make a large, stand alone REST API.
Navigate to http://localhost:8000/apps/earth-engine/api/get-time-series/. You should see an API page that is auto generated by the Django REST Framework titled Get Time Series. The page should display an HTTP 401 Unauthorized error and display a result object with detail "Authentication credentials were not provided."
5. Test with Postman Application
Most web browsers are surprisingly limited when it comes to testing REST APIs. The reason the test in the previous step resulted in a 401 Unauthorized is because we sent a request without an authentication token. To more easily test this, you'll want to get a REST client that will allow you to set request headers and parameters. In this tutorial you will use the Postman client to test the REST API as you develop it.
If you have not done so already, download and install the Postman app and then launch it.
In Postman click on the New button and select Collection.
Name the collection "Earth Engine App API" and press the Create button.
Right-click on the new Earth Engine App API collection or click on it's "..." button and select Add Request.
Name the new request "get-time-series" and press the Save to Earth Engine App API button.
From the menu on the left, expand the Earth Engine App API collection and click on the get-time-series request to open it in a new tab.
Select GET as the method and enter "http://localhost:8000/apps/earth-engine/api/get-time-series/" in the URL field.
Press the Save button to save changes.
Press the Send button. You should see the same response object as before with the "Authentication credentials were not provided." message.
7. Define Parameters for REST API
In this step you'll define the parameters that the REST endpoint will accept. If you think of the REST endpoint as a function, then the parameters are like the arguments to the function. The controller will be configured to work with both the GET
and POST
methods for illustration purposes.
Update the
get_time_series
controller incontrollers/rest.py
as follows:
from django.http import HttpResponseBadRequest
@controller(url='api/get-time-series', login_required=False)
@api_view(['GET', 'POST'])
@authentication_classes((TokenAuthentication,))
def get_time_series(request):
"""
Controller for the get-time-series REST endpoint.
"""
# Get request parameters.
if request.method == 'GET':
data = request.GET.copy()
elif request.method == 'POST':
data = request.POST.copy()
else:
return HttpResponseBadRequest('Only GET and POST methods are supported.')
platform = data.get('platform', None)
sensor = data.get('sensor', None)
product = data.get('product', None)
start_date_str = data.get('start_date', None)
end_date_str = data.get('end_date', None)
reducer = data.get('reducer', 'median')
index = data.get('index', None)
scale_str = data.get('scale', 250)
orient = data.get('orient', 'list')
geometry_str = data.get('geometry', None)
# compose response object.
response_data = {
'parameters': {
'platform': platform,
'sensor': sensor,
'product': product,
'index': index,
'start_date': start_date_str,
'end_date': end_date_str,
'reducer': reducer,
'geometry': geometry_str
}
}
return JsonResponse(response_data)
In Postman, select the Params tab.
Click on the Bulk Edit link on the right and enter the following:
platform:modis
sensor:terra
product:temperature
start_date:2020-02-15
end_date:2020-04-14
reducer:mean
//index:NDVI
geometry:{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[36.112060546875,-0.03295898255728957],"properties":{"id":"drawing_layer.79c08238-4084-4825-9e76-f018527d45b7"},"crs":{"type":"link","properties":{"href":"http://spatialreference.org/ref/epsg/4326/proj4/","type":"proj4"}}},{"type":"Polygon","coordinates":[[[36.749267578125,0.1867672473697155],[36.6943359375,-0.043945308191354115],[36.99096679687499,-0.043945308191354115],[36.9140625,0.1757809742470755],[36.749267578125,0.1867672473697155]]],"properties":{"id":"drawing_layer.ffa36dfd-5767-4946-890b-f4c0d9c0ff9f"},"crs":{"type":"link","properties":{"href":"http://spatialreference.org/ref/epsg/4326/proj4/","type":"proj4"}}}]}
orient:series
scale:250
Click on the Key-Value Edit link on the right. Notice how the Query Params key-value form is populated with values. Also, notice that the same parameters are added to the URL as query parameters (i.e.
?key1=value1&key2=value2
).Note
The
index
parameter should be unchecked / disabled.Press the Send button and verify that the parameters are returned in the response object.
Press the Save button to save your changes to the Postman request.
8. Validate Platform, Sensor, Product, and Index
In this step you'll add the validation logic for the platform
, sensor
, product
, and index
parameters. The REST endpoint is like a function shared publicly on the internet--anyone can call it with whatever parameters they want. This includes bots that may try to exploit your website through its REST endpoints. Be sure to only allow valid values through and provide helpful feedback for users of the REST API.
Modify the
get_time_series
controller incontrollers/rest.py
to add validation for theplatform
,sensor
,product
, andindex
parameters as follows:
from ..gee.products import EE_PRODUCTS
@controller(url='api/get-time-series')
@api_view(['GET', 'POST'])
@authentication_classes((TokenAuthentication,))
def get_time_series(request):
"""
Controller for the get-time-series REST endpoint.
"""
# Get request parameters.
if request.method == 'GET':
data = request.GET.copy()
elif request.method == 'POST':
data = request.POST.copy()
else:
return HttpResponseBadRequest('Only GET and POST methods are supported.')
platform = data.get('platform', None)
sensor = data.get('sensor', None)
product = data.get('product', None)
start_date_str = data.get('start_date', None)
end_date_str = data.get('end_date', None)
reducer = data.get('reducer', 'median')
index = data.get('index', None)
scale_str = data.get('scale', 250)
orient = data.get('orient', 'dict')
geometry_str = data.get('geometry', None)
# validate given parameters
# platform
if not platform or platform not in EE_PRODUCTS:
valid_platform_str = '", "'.join(EE_PRODUCTS.keys())
return HttpResponseBadRequest(f'The "platform" parameter is required. Valid platforms '
f'include: "{valid_platform_str}".')
# sensors
if not sensor or sensor not in EE_PRODUCTS[platform]:
valid_sensor_str = '", "'.join(EE_PRODUCTS[platform].keys())
return HttpResponseBadRequest(f'The "sensor" parameter is required. Valid sensors for the "{platform}" '
f'platform include: "{valid_sensor_str}".')
# product
if not product or product not in EE_PRODUCTS[platform][sensor]:
valid_product_str = '", "'.join(EE_PRODUCTS[platform][sensor].keys())
return HttpResponseBadRequest(f'The "product" parameter is required. Valid products for the "{platform} '
f'{sensor}" sensor include: "{valid_product_str}".')
selected_product = EE_PRODUCTS[platform][sensor][product]
# index
# if index not provided, get default index from product properties
if not index:
index = selected_product['index']
# if index is still None (not defined for the product) it is not supported currently
if index is None:
return HttpResponseBadRequest(
f'Retrieving time series for "{platform} {sensor} {product}" is not supported at this time.'
)
# compose response object.
response_data = {
'parameters': {
'platform': platform,
'sensor': sensor,
'product': product,
'index': index,
'start_date': start_date_str,
'end_date': end_date_str,
'reducer': reducer,
'geometry': geometry_str
}
}
return JsonResponse(response_data)
In Postman, select the Params tab if not already active.
Uncheck all of the parameters so that they are not included in the request.
Press the Send button and verify that the status code 400 Bad Request is returned (see top-right side of the response section) and the validation message for the
platform
parameter is returned.Add the
platform
parameter to the request by checking the box next to it.Press the Send button and verify that the status code 400 Bad Request is returned (see top-right side of the response section) and the validation message for the
sensor
parameter is returned.Change the value of the
platform
parameter to "landsat" or "sentinel" and verify that the validation message for thesensor
parameter lists the appropriate sensors.Change the
platform
parameter back to "modis".Repeat this process, adding first the
sensor
parameter, then theproduct
parameter to confirm that the validation logic is working as expected.
9. Validate Dates
In this step you'll add the validation logic for the start_date
and end_date
parameters. There is logic that already exists in the viewer
controller that you can use to validate the date parameters in our REST API function. However, you should avoid copying code to prevent duplicating bugs and make the app easier to maintain. Instead, you will generalize the bit of code from the viewer
controller into a helper function and then use that function in both the viewer
controller and the get_time_series
controller.
Create a new helper function called
compute_dates_for_product
inhelpers.py
with contents based on the validation logic for dates in theviewer
controller:
import datetime as dt
def compute_dates_for_product(product_dict):
"""
Compute default dates and date range for given product.
Args:
product_dict (dict): The product dictionary from EE_PRODUCTS
Returns:
dict<default_start_date,default_end_date,beg_valid_date_range,end_valid_date_range>: dict with date strings formatted: %Y-%m-%d.
"""
# Hardcode initial end date to today (since all of our datasets extend to present)
today = dt.datetime.today()
default_end_date = today.strftime('%Y-%m-%d')
# Initial start date will a set number of days before the end date
# NOTE: This assumes the start date of the dataset is at least 30+ days prior to today
default_end_date_dt = dt.datetime.strptime(default_end_date, '%Y-%m-%d')
default_start_date_dt = default_end_date_dt - dt.timedelta(days=30)
default_start_date = default_start_date_dt.strftime('%Y-%m-%d')
# Get valid date range for product
beg_valid_date_range = product_dict.get('start_date', None)
end_valid_date_range = product_dict.get('end_date', None) or default_end_date
product_dates = {
'default_start_date': default_start_date,
'default_end_date': default_end_date,
'beg_valid_date_range': beg_valid_date_range,
'end_valid_date_range': end_valid_date_range
}
return product_dates
Tip
Compare this function with similar logic in the viewer
controller. Many of the variables have been renamed to make it more general, but the functionality is mostly the same.
Import the new
compute_dates_for_product
helper function and then refactor theviewer
controller incontrollers/viewer.py`
to use the newcompute_dates_for_product
helper function. Replace all of the previous date logic in theviewer
controller starting with the line with comment# Hardcode initial end date ...
and ending with theend_date
DatePicker
:
from ..helpers import compute_dates_for_product
-# Hardcode initial end date to today (since all of our datasets extend to present)
-today = dt.datetime.today()
-initial_end_date = today.strftime('%Y-%m-%d')
-# Initial start date will a set number of days before the end date
-# NOTE: This assumes the start date of the dataset is at least 30+ days prior to today
-initial_end_date_dt = dt.datetime.strptime(initial_end_date, '%Y-%m-%d')
-initial_start_date_dt = initial_end_date_dt - dt.timedelta(days=30)
-initial_start_date = initial_start_date_dt.strftime('%Y-%m-%d')
-# Build date controls
-first_product_start_date = first_product.get('start_date', None)
-first_product_end_date = first_product.get('end_date', None) or initial_end_date
+# Get initial default dates and date ranges for date picker controls
+first_product_dates = compute_dates_for_product(first_product)
start_date = DatePicker(
name='start_date',
display_text='Start Date',
format='yyyy-mm-dd',
start_view='decade',
today_button=True,
today_highlight=True,
+ start_date=first_product_dates['beg_valid_date_range'],
+ end_date=first_product_dates['end_valid_date_range'],
+ initial=first_product_dates['default_start_date'],
autoclose=True
)
end_date = DatePicker(
name='end_date',
display_text='End Date',
format='yyyy-mm-dd',
start_view='decade',
today_button=True,
today_highlight=True,
+ start_date=first_product_dates['beg_valid_date_range'],
+ end_date=first_product_dates['end_valid_date_range'],
+ initial=first_product_dates['default_end_date'],
autoclose=True
)
Modify the
get_time_series
controller incontrollers/rest.py
to also use thecompute_dates_for_product
helper function as part of it's validation for thestart_date
andend_date
parameters. Replace theresponse_data
object with the following:
import datetime as dt
from ..helpers import compute_dates_for_product
# get valid dates for selected product
product_dates = compute_dates_for_product(selected_product)
# assign default start date if not provided
if not start_date_str:
start_date_str = product_dates['default_start_date']
# assign default start date if not provided
if not end_date_str:
end_date_str = product_dates['default_end_date']
# convert to datetime objects for validation
try:
start_date_dt = dt.datetime.strptime(start_date_str, '%Y-%m-%d')
end_date_dt = dt.datetime.strptime(end_date_str, '%Y-%m-%d')
except ValueError:
return HttpResponseBadRequest(
'Invalid date format. Please use "YYYY-MM-DD".'
)
beg_valid_date_range = dt.datetime.strptime(product_dates['beg_valid_date_range'], '%Y-%m-%d')
end_valid_date_range = dt.datetime.strptime(product_dates['end_valid_date_range'], '%Y-%m-%d')
# start_date in valid range
if start_date_dt < beg_valid_date_range or start_date_dt > end_valid_date_range:
return HttpResponseBadRequest(
f'The date {start_date_str} is not a valid "start_date" for "{platform} {sensor} {product}". '
f'It must occur between {product_dates["beg_valid_date_range"]} '
f'and {product_dates["end_valid_date_range"]}.'
)
# end_date in valid range
if end_date_dt < beg_valid_date_range or end_date_dt > end_valid_date_range:
return HttpResponseBadRequest(
f'The date {end_date_str} is not a valid "end_date" for "{platform} {sensor} {product}". '
f'It must occur between {product_dates["beg_valid_date_range"]} '
f'and {product_dates["end_valid_date_range"]}.'
)
# start_date before end_date
if start_date_dt > end_date_dt:
return HttpResponseBadRequest(
f'The "start_date" must occur before the "end_date". Dates given: '
f'start_date = {start_date_str}; end_date = {end_date_str}.'
)
# compose response object.
response_data = {
'parameters': {
'platform': platform,
'sensor': sensor,
'product': product,
'index': index,
'start_date': start_date_str,
'end_date': end_date_str,
'reducer': reducer,
'geometry': geometry_str
}
}
Use Postman to send a request with only the
platform
,sensor
, andproduct
parameters. Ensure that the values given for the enabled parameters are valid. Verify thatend_date
is returned as today's date and that thestart_date
is 30 days prior to today's date.Add the
start_date
parameter and send another request. Verify that the same date sent is returned as thestart_date
.Add the
end_date
parameter and send another request. Verify that the same date sent is returned as theend_date
.Also test different values for dates to test the following scenarios:
start_date
==end_date
start_date
>end_date
start_date
outside of valid range of selected product (seegee/products.py
)end_date
outside of valid range of selected product (seegee/products.py
)start_date
andend_date
outside of valid range of selected product (seegee/products.py
)Incorrect date format given for either date parameter
10. Validate Reducer, Orient, and Scale
In this step you'll add the validation logic for the reducer
, orient
, and scale
parameters. The reducer
and orient
parameters each have a short list of valid options and the scale
parameter needs to be a number.
Modify the
get_time_series
controller incontrollers/rest.py
to add validation for thereducer
,orient
, andscale
parameters. Replace theresponse_data
object with the following:
# reducer
valid_reducers = ('median', 'mosaic', 'mode', 'mean', 'min', 'max', 'sum', 'count', 'product')
if reducer not in valid_reducers:
valid_reducer_str = '", "'.join(valid_reducers)
return HttpResponseBadRequest(
f'The value "{reducer}" is not valid for parameter "reducer". '
f'Must be one of: "{valid_reducer_str}". Defaults to "median" '
f'if not given.'
)
# orient
valid_orient_vals = ('dict', 'list', 'series', 'split', 'records', 'index')
if orient not in valid_orient_vals:
valid_orient_str = '", "'.join(valid_orient_vals)
return HttpResponseBadRequest(
f'The value "{orient}" is not valid for parameter "orient". '
f'Must be one of: "{valid_orient_str}". Defaults to "dict" '
f'if not given.'
)
# scale
try:
scale = float(scale_str)
except ValueError:
return HttpResponseBadRequest(
f'The "scale" parameter must be a valid number, but "{scale_str}" was given.'
)
# compose response object.
response_data = {
'parameters': {
'platform': platform,
'sensor': sensor,
'product': product,
'index': index,
'start_date': start_date_str,
'end_date': end_date_str,
'reducer': reducer,
'orient': orient,
'scale': scale,
'geometry_str': geometry_str
}
}
Use Postman to send a request with only the
platform
,sensor
,product
,start_date
andend_date
parameters. Ensure that the values given for the enabled parameters are valid. Verify that the default values forreducer
,orient
, andscale
are returned.Add the
reducer
parameter with an invalid value (e.g.foo
). Verify that the validation message is displayed and lists valid values forreducer
.Change
reducer
to a valid value other than the default (e.g.:mean
). Verify this value is returned.Add the
orient
parameter with an invalid value (e.g.foo
). Verify that the validation message is displayed and lists valid values fororient
.Change
orient
to a valid value other than the default (e.g.:series
). Verify this value is returned.Add the
scale
parameter with a non-numeric value (e.g.:foo
). Verify that the validation message is displayed forscale
.Change
scale
to a valid value other than the default (e.g.:150
). Verify this value is returned.
11. Validate Geometry
In this step you'll add the logic to validate the geometry
parameter, which should be valid GeoJSON. An optimistic strategy will be used in which an attempt will be made to convert the string into a GeoJSON object. If it fails, then the given string is not valid GeoJSON and an error will be returned.
Modify the
get_time_series
controller incontrollers/rest.py
to add validation for thegeometry
parameter. Replace theresponse_data
object with the following:
import geojson
from simplejson import JSONDecodeError
# geometry
bad_geometry_msg = 'The "geometry" parameter is required and must be a valid geojson string.'
if not geometry_str:
return HttpResponseBadRequest(bad_geometry_msg)
try:
geometry = geojson.loads(geometry_str)
except JSONDecodeError:
return HttpResponseBadRequest(bad_geometry_msg)
# compose response object.
response_data = {
'parameters': {
'platform': platform,
'sensor': sensor,
'product': product,
'index': index,
'start_date': start_date_str,
'end_date': end_date_str,
'reducer': reducer,
'orient': orient,
'scale': scale,
'geometry': geometry
}
}
Use Postman to send a request with only the
platform
,sensor
,product
,start_date
,end_date
,reducer
,orient
, andscale
parameters. Ensure that the values given for the enabled parameters are valid. Verify that a message indicating that thegeometry
parameter is required is returned.Add the
geometry
parameter with an invalid value (e.g.foo
). Verify that the validation message is displayed and indicates that the geometry parameter must be GeoJSON.Change
geometry
to the following and verify this value is returned:
{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[36.112060546875,-0.03295898255728957],"properties":{"id":"drawing_layer.79c08238-4084-4825-9e76-f018527d45b7"},"crs":{"type":"link","properties":{"href":"http://spatialreference.org/ref/epsg/4326/proj4/","type":"proj4"}}},{"type":"Polygon","coordinates":[[[36.749267578125,0.1867672473697155],[36.6943359375,-0.043945308191354115],[36.99096679687499,-0.043945308191354115],[36.9140625,0.1757809742470755],[36.749267578125,0.1867672473697155]]],"properties":{"id":"drawing_layer.ffa36dfd-5767-4946-890b-f4c0d9c0ff9f"},"crs":{"type":"link","properties":{"href":"http://spatialreference.org/ref/epsg/4326/proj4/","type":"proj4"}}}]}
Important
When pasting the geometry
value from above, ensure that there are no new lines / returns after (i.e. press Backspace after pasting).
12. Reuse Existing Helper Function to Get Time Series
With the parameters properly vetted, you are now ready to call the get_time_series_from_image_collection
function. It should be a fairly straightforward call of the function, mapping the REST parameters to the arguments of the function. You will need to make a few minor changes to the function, however, to accommodate the new orient
option.
Refactor the
get_time_series_from_image_collection
function ingee/methods.py
to accept theorient
argument by replacing the function with this new definition:
def get_time_series_from_image_collection(platform, sensor, product, index_name, scale=30, geometry=None,
date_from=None, date_to=None, reducer='median', orient='df'):
"""
Derive time series at given geometry.
"""
time_series = []
ee_product = EE_PRODUCTS[platform][sensor][product]
collection_name = ee_product['collection']
if not isinstance(geometry, geojson.GeometryCollection):
raise ValueError('Geometry must be a valid GeoJSON GeometryCollection.')
for geom in geometry.geometries:
log.debug(f'Computing Time Series for Geometry of Type: {geom.type}')
try:
ee_geometry = None
if isinstance(geom, geojson.Polygon):
ee_geometry = ee.Geometry.Polygon(geom.coordinates)
elif isinstance(geom, geojson.Point):
ee_geometry = ee.Geometry.Point(geom.coordinates)
else:
raise ValueError('Only Points and Polygons are supported.')
if date_from is not None:
if index_name is not None:
indexCollection = ee.ImageCollection(collection_name) \
.filterDate(date_from, date_to) \
.select(index_name)
else:
indexCollection = ee.ImageCollection(collection_name) \
.filterDate(date_from, date_to)
else:
indexCollection = ee.ImageCollection(collection_name)
def get_index(image):
if reducer:
the_reducer = getattr(ee.Reducer, reducer)()
if index_name is not None:
index_value = image.reduceRegion(the_reducer, ee_geometry, scale).get(index_name)
else:
index_value = image.reduceRegion(the_reducer, ee_geometry, scale)
date = image.get('system:time_start')
index_image = ee.Image().set('indexValue', [ee.Number(date), index_value])
return index_image
index_collection = indexCollection.map(get_index)
index_collection_agg = index_collection.aggregate_array('indexValue')
values = index_collection_agg.getInfo()
log.debug('Values acquired.')
df = pd.DataFrame(values, columns=['Time', index_name.replace("_", " ")])
if orient == 'df':
time_series.append(df)
else:
time_series.append(df.to_dict(orient=orient))
except EEException:
log.exception('An error occurred while attempting to retrieve the time series.')
log.debug(f'Time Series: {time_series}')
return time_series
Note
You don't need to worry about updating existing calls of get_time_series_from_image_collection
, because the new orient
argument was added at the end of the argument list with a default value that will cause it to behave as it did before the argument was added.
Modify the
get_time_series
controller incontrollers/rest.py
to call theget_time_series_from_image_collection
function and return the time series in the response object. Replace theresponse_data
object with the following:
from django.http import HttpResponseServerError
from ..gee.methods import get_time_series_from_image_collection
try:
time_series = get_time_series_from_image_collection(
platform=platform,
sensor=sensor,
product=product,
index_name=index,
scale=scale,
geometry=geometry,
date_from=start_date_str,
date_to=end_date_str,
reducer=reducer,
orient=orient
)
except ValueError as e:
return HttpResponseBadRequest(str(e))
except Exception:
log.exception('An unexpected error occurred during execution of get_time_series_from_image_collection.')
return HttpResponseServerError('An unexpected error occurred. Please review your parameters and try again.')
# compose response object.
response_data = {
'time_series': time_series,
'parameters': {
'platform': platform,
'sensor': sensor,
'product': product,
'index': index,
'start_date': start_date_str,
'end_date': end_date_str,
'reducer': reducer,
'orient': orient,
'scale': scale,
'geometry': geometry
}
}
Enable all of the Query parameters in Postman by checking the box next to each with the exception of the
index
parameter. Ensure that the values given for the enabled parameters are valid.Press the Send button to submit the request and verify that the time series is included in the response object.
13. Test & Verify
Use Postman to try different values for each of the parameters. Use some that are valid and others that are not to ensure the validation is working.
Switch the method from "GET" to "POST".
Uncheck all of the Query parameters in the Params tab.
Select the Body tab and toggle on the form-data radio button.
Press the Bulk Edit link at the right and insert the following:
platform:modis
sensor:terra
product:temperature
start_date:2020-02-15
end_date:2020-04-14
reducer:mean
//index:NDVI
geometry:{"type":"GeometryCollection","geometries":[{"type":"Point","coordinates":[36.112060546875,-0.03295898255728957],"properties":{"id":"drawing_layer.79c08238-4084-4825-9e76-f018527d45b7"},"crs":{"type":"link","properties":{"href":"http://spatialreference.org/ref/epsg/4326/proj4/","type":"proj4"}}},{"type":"Polygon","coordinates":[[[36.749267578125,0.1867672473697155],[36.6943359375,-0.043945308191354115],[36.99096679687499,-0.043945308191354115],[36.9140625,0.1757809742470755],[36.749267578125,0.1867672473697155]]],"properties":{"id":"drawing_layer.ffa36dfd-5767-4946-890b-f4c0d9c0ff9f"},"crs":{"type":"link","properties":{"href":"http://spatialreference.org/ref/epsg/4326/proj4/","type":"proj4"}}}]}
orient:series
scale:250
Press the Send button to ensure the API works as expected with the POST method. The response should look similar to the following:
{
"time_series": [
{
"Time": "{\"0\":1581724800000,\"1\":1581811200000,\"2\":1581897600000,\"3\":1581984000000,\"4\":1582070400000,\"5\":1582156800000,\"6\":1582243200000,\"7\":1582329600000,\"8\":1582416000000,\"9\":1582502400000,\"10\":1582588800000,\"11\":1582675200000,\"12\":1582761600000,\"13\":1582848000000,\"14\":1582934400000,\"15\":1583020800000,\"16\":1583107200000,\"17\":1583193600000,\"18\":1583280000000,\"19\":1583366400000,\"20\":1583452800000,\"21\":1583539200000,\"22\":1583625600000,\"23\":1583712000000,\"24\":1583798400000,\"25\":1583884800000,\"26\":1583971200000,\"27\":1584057600000,\"28\":1584144000000,\"29\":1584230400000,\"30\":1584316800000,\"31\":1584403200000,\"32\":1584489600000,\"33\":1584576000000,\"34\":1584662400000,\"35\":1584748800000,\"36\":1584835200000,\"37\":1584921600000,\"38\":1585008000000,\"39\":1585094400000,\"40\":1585180800000,\"41\":1585267200000,\"42\":1585353600000,\"43\":1585440000000,\"44\":1585526400000,\"45\":1585612800000,\"46\":1585699200000,\"47\":1585785600000,\"48\":1585872000000,\"49\":1585958400000,\"50\":1586044800000,\"51\":1586131200000,\"52\":1586217600000,\"53\":1586304000000,\"54\":1586390400000,\"55\":1586476800000,\"56\":1586563200000,\"57\":1586649600000,\"58\":1586736000000}",
"LST Day 1km": "{\"0\":null,\"1\":null,\"2\":15178.0,\"3\":15046.0,\"4\":14882.0,\"5\":null,\"6\":15409.0,\"7\":15030.0,\"8\":null,\"9\":15091.0,\"10\":null,\"11\":null,\"12\":null,\"13\":15470.0,\"14\":15252.0,\"15\":15511.0,\"16\":null,\"17\":null,\"18\":null,\"19\":null,\"20\":15595.0,\"21\":null,\"22\":15197.0,\"23\":null,\"24\":null,\"25\":15024.0,\"26\":14907.0,\"27\":15346.0,\"28\":null,\"29\":15627.0,\"30\":15120.0,\"31\":15024.0,\"32\":null,\"33\":null,\"34\":15139.0,\"35\":15090.0,\"36\":15626.0,\"37\":null,\"38\":15224.0,\"39\":null,\"40\":15013.0,\"41\":null,\"42\":null,\"43\":null,\"44\":null,\"45\":15295.0,\"46\":null,\"47\":15368.0,\"48\":15342.0,\"49\":null,\"50\":15053.0,\"51\":null,\"52\":15189.0,\"53\":null,\"54\":15094.0,\"55\":15107.0,\"56\":15415.0,\"57\":15263.0,\"58\":null}"
},
{
"Time": "{\"0\":1581724800000,\"1\":1581811200000,\"2\":1581897600000,\"3\":1581984000000,\"4\":1582070400000,\"5\":1582156800000,\"6\":1582243200000,\"7\":1582329600000,\"8\":1582416000000,\"9\":1582502400000,\"10\":1582588800000,\"11\":1582675200000,\"12\":1582761600000,\"13\":1582848000000,\"14\":1582934400000,\"15\":1583020800000,\"16\":1583107200000,\"17\":1583193600000,\"18\":1583280000000,\"19\":1583366400000,\"20\":1583452800000,\"21\":1583539200000,\"22\":1583625600000,\"23\":1583712000000,\"24\":1583798400000,\"25\":1583884800000,\"26\":1583971200000,\"27\":1584057600000,\"28\":1584144000000,\"29\":1584230400000,\"30\":1584316800000,\"31\":1584403200000,\"32\":1584489600000,\"33\":1584576000000,\"34\":1584662400000,\"35\":1584748800000,\"36\":1584835200000,\"37\":1584921600000,\"38\":1585008000000,\"39\":1585094400000,\"40\":1585180800000,\"41\":1585267200000,\"42\":1585353600000,\"43\":1585440000000,\"44\":1585526400000,\"45\":1585612800000,\"46\":1585699200000,\"47\":1585785600000,\"48\":1585872000000,\"49\":1585958400000,\"50\":1586044800000,\"51\":1586131200000,\"52\":1586217600000,\"53\":1586304000000,\"54\":1586390400000,\"55\":1586476800000,\"56\":1586563200000,\"57\":1586649600000,\"58\":1586736000000}",
"LST Day 1km": "{\"0\":14968.8013557598,\"1\":14732.0,\"2\":15140.6162672913,\"3\":14964.0387762783,\"4\":14997.5439551696,\"5\":null,\"6\":15329.096412742,\"7\":15040.9105709928,\"8\":14961.5659903202,\"9\":15247.6460587379,\"10\":15128.6494517054,\"11\":null,\"12\":null,\"13\":15315.9218134749,\"14\":15142.2256710748,\"15\":15489.6098782062,\"16\":null,\"17\":null,\"18\":14830.7316079983,\"19\":null,\"20\":15365.3874342389,\"21\":null,\"22\":15096.8693791135,\"23\":null,\"24\":14571.8987736331,\"25\":14941.6888052079,\"26\":14906.3561937113,\"27\":15206.5296095194,\"28\":null,\"29\":15568.0890033355,\"30\":15302.2537246606,\"31\":15168.7467805083,\"32\":14991.9650580776,\"33\":15031.5642354043,\"34\":14976.2529256142,\"35\":15015.4395296379,\"36\":15419.699948541,\"37\":null,\"38\":15306.6092905512,\"39\":15305.2591368269,\"40\":null,\"41\":null,\"42\":null,\"43\":14921.4426529555,\"44\":null,\"45\":15293.5047969806,\"46\":null,\"47\":15155.7042583175,\"48\":15072.2772985564,\"49\":14963.3847646173,\"50\":14974.3150231811,\"51\":null,\"52\":15099.3012719277,\"53\":null,\"54\":15242.5142541762,\"55\":15089.3998174908,\"56\":15442.3522075961,\"57\":15175.1380971884,\"58\":null}"
}
],
"parameters": {
"platform": "modis",
"sensor": "terra",
"product": "temperature",
"index": "LST_Day_1km",
"start_date": "2020-02-15",
"end_date": "2020-04-14",
"reducer": "mean",
"orient": "series",
"scale": 250.0,
"geometry": {
"type": "GeometryCollection",
"geometries": [
{
"type": "Point",
"properties": {
"id": "drawing_layer.79c08238-4084-4825-9e76-f018527d45b7"
},
"crs": {
"type": "link",
"properties": {
"href": "http://spatialreference.org/ref/epsg/4326/proj4/",
"type": "proj4"
}
},
"coordinates": [
36.112061,
-0.032959
]
},
{
"type": "Polygon",
"properties": {
"id": "drawing_layer.ffa36dfd-5767-4946-890b-f4c0d9c0ff9f"
},
"crs": {
"type": "link",
"properties": {
"href": "http://spatialreference.org/ref/epsg/4326/proj4/",
"type": "proj4"
}
},
"coordinates": [
[
[
36.749268,
0.186767
],
[
36.694336,
-0.043945
],
[
36.990967,
-0.043945
],
[
36.914062,
0.175781
],
[
36.749268,
0.186767
]
]
]
}
]
}
}
}
14. Solution
This concludes this portion of the GEE Tutorial. You can view the solution on GitHub at https://github.com/tethysplatform/tethysapp-earth_engine/tree/rest-api-solution-solution-3.0 or clone it as follows:
git clone https://github.com/tethysplatform/tethysapp-earth_engine.git cd tethysapp-earth_engine git checkout -b rest-api-solution rest-api-solution-0.1