Skip to content

Commit

Permalink
Initial Clip by Asset Solution
Browse files Browse the repository at this point in the history
  • Loading branch information
swainn committed May 7, 2020
1 parent 95c3eaf commit e07e34a
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 5 deletions.
22 changes: 18 additions & 4 deletions tethysapp/earth_engine/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import datetime as dt
import geojson
import ee
import shapefile
from django.http import JsonResponse, HttpResponseNotAllowed, HttpResponseRedirect
from django.shortcuts import render
Expand All @@ -12,7 +13,8 @@
from tethys_sdk.permissions import login_required
from tethys_sdk.workspaces import user_workspace
from .helpers import generate_figure, find_shapefile, write_boundary_shapefile, prep_boundary_dir
from .gee.methods import get_image_collection_asset, get_time_series_from_image_collection
from .gee.methods import get_image_collection_asset, get_time_series_from_image_collection, upload_shapefile_to_gee, \
get_boundary_fc_props_for_user
from .gee.products import EE_PRODUCTS

log = logging.getLogger(f'tethys.apps.{__name__}')
Expand Down Expand Up @@ -162,14 +164,17 @@ def viewer(request, user_workspace):
attributes={'id': 'load_plot'}
)

# Get bounding box from user boundary if it exists
boundary_props = get_boundary_fc_props_for_user(request.user)

map_view = MapView(
height='100%',
width='100%',
controls=[
'ZoomSlider', 'Rotate', 'FullScreen',
{'ZoomToExtent': {
'projection': 'EPSG:4326',
'extent': [29.25, -4.75, 46.25, 5.2]
'extent': boundary_props.get('bbox', [-180, -90, 180, 90]) # Default to World
}}
],
basemap=[
Expand All @@ -181,8 +186,8 @@ def viewer(request, user_workspace):
],
view=MVView(
projection='EPSG:4326',
center=[37.880859, 0.219726],
zoom=7,
center=boundary_props.get('centroid', [0, 0]), # Default to World
zoom=boundary_props.get('zoom', 3), # Default to World
maxZoom=18,
minZoom=2
),
Expand Down Expand Up @@ -254,6 +259,7 @@ def get_image_collection(request):
reducer = request.POST.get('reducer', None)

url = get_image_collection_asset(
request=request,
platform=platform,
sensor=sensor,
product=product,
Expand Down Expand Up @@ -397,5 +403,13 @@ def handle_shapefile_upload(request, user_workspace):
# Write the shapefile to the workspace directory
write_boundary_shapefile(shp_file, workspace_dir)

# Upload shapefile as Asset in GEE
upload_shapefile_to_gee(request.user, shp_file)

except TypeError:
return 'Incomplete or corrupted shapefile provided.'

except ee.EEException:
msg = 'An unexpected error occurred while uploading the shapefile to Google Earth Engine.'
log.exception(msg)
return msg
182 changes: 181 additions & 1 deletion tethysapp/earth_engine/gee/methods.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import os
import math
import logging
import ee
from ee.ee_exception import EEException
Expand Down Expand Up @@ -44,7 +46,7 @@ def image_to_map_id(image_name, vis_params={}):
log.exception('An error occurred while attempting to retrieve the map id.')


def get_image_collection_asset(platform, sensor, product, date_from=None, date_to=None, reducer='median'):
def get_image_collection_asset(request, platform, sensor, product, date_from=None, date_to=None, reducer='median'):
"""
Get tile url for image collection asset.
"""
Expand Down Expand Up @@ -77,6 +79,12 @@ def get_image_collection_asset(platform, sensor, product, date_from=None, date_t
if reducer:
ee_collection = getattr(ee_collection, reducer)()

# Attempt to clip the image by the boundary provided by the user
clip_features = get_boundary_fc_for_user(request.user)

if clip_features:
ee_collection = ee_collection.clipToCollection(clip_features)

tile_url = image_to_map_id(ee_collection, vis_params)

return tile_url
Expand Down Expand Up @@ -145,3 +153,175 @@ def get_index(image):

log.debug(f'Time Series: {time_series}')
return time_series


def upload_shapefile_to_gee(user, shp_file):
"""
Upload a shapefile to Google Earth Engine as an asset.
Args:
user (django.contrib.auth.User): the request user.
shp_file (shapefile.Reader): A shapefile reader object.
"""
features = []
fields = shp_file.fields[1:]
field_names = [field[0] for field in fields]

# Convert Shapefile to ee.Features
for record in shp_file.shapeRecords():
# First convert to geojson
attributes = dict(zip(field_names, record.record))
geojson_geom = record.shape.__geo_interface__
geojson_feature = {
'type': 'Feature',
'geometry': geojson_geom,
'properties': attributes
}

# Create ee.Feature from geojson (this is the Upload, b/c ee.Feature is a server object)
features.append(ee.Feature(geojson_feature))

feature_collection = ee.FeatureCollection(features)

# Get unique folder for each user to story boundary asset
user_boundary_asset_path = get_user_boundary_path(user)

# Overwrite an existing asset with this name by deleting it first
try:
ee.batch.data.deleteAsset(user_boundary_asset_path)
except EEException as e:
# Nothing to delete, so pass
if 'Asset not found' not in str(e):
log.exception('Encountered an unhandled EEException.')
raise e

# Export ee.Feature to ee.Asset
task = ee.batch.Export.table.toAsset(
collection=feature_collection,
description='uploadToTableAsset',
assetId=user_boundary_asset_path
)

task.start()


def get_asset_dir_for_user(user):
"""
Get a unique asset directory for given user.
Args:
user (django.contrib.auth.User): the request user.
Returns:
str: asset directory path for given user.
"""
asset_roots = ee.batch.data.getAssetRoots()

if len(asset_roots) < 1:
# Initialize the asset root directory if one doesn't exist already
ee.batch.data.createAssetHome('users/earth_engine_app')

asset_root_dir = asset_roots[0]['id']
earth_engine_root_dir = os.path.join(asset_root_dir, 'earth_engine_app')
user_root_dir = os.path.join(earth_engine_root_dir, user.username)

# Create earth engine directory, will raise exception if it already exists
try:
ee.batch.data.createAsset({
'type': 'Folder',
'name': earth_engine_root_dir
})
except EEException as e:
if 'Cannot overwrite asset' not in str(e):
raise e

# Create user directory, will raise exception if it already exists
try:
ee.batch.data.createAsset({
'type': 'Folder',
'name': user_root_dir
})
except EEException as e:
if 'Cannot overwrite asset' not in str(e):
raise e

return user_root_dir


def get_user_boundary_path(user):
"""
Get a unique path for the user boundary asset.
Args:
user (django.contrib.auth.User): the request user.
Returns:
str: the unique path for the user boundary asset.
"""
user_asset_dir = get_asset_dir_for_user(user)
user_boundary_asset_path = os.path.join(user_asset_dir, 'boundary')
return user_boundary_asset_path


def get_boundary_fc_for_user(user):
"""
Get the boundary FeatureClass for the given user if it exists.
Args:
user (django.contrib.auth.User): the request user.
Returns:
ee.FeatureCollection: boundary feature collection or None
"""
try:
boundary_path = get_user_boundary_path(user)
# If no boundary exists for the user, an exception occur when calling this and clipping will skipped
ee.batch.data.getAsset(boundary_path)
# Add the clip option
fc = ee.FeatureCollection(boundary_path)
return fc
except EEException:
pass

return None


def get_boundary_fc_props_for_user(user):
"""
Get various properties of the boundary FeatureCollection.
Args:
user (django.contrib.auth.User): Get the properties of the boundary uploaded by this user.
Returns:
dict<zoom,bbox,centroid>: Dictionary containing the centroid and bounding box of the boundary and the approximate OpenLayers zoom level to frame the boundary around the centroid. Empty dictionary if no boundary FeatureCollection is found for the given user.
"""
fc = get_boundary_fc_for_user(user)

if not fc:
return dict()

# Compute bounding box
bounding_rect = fc.geometry().bounds().getInfo()
bounding_coords = bounding_rect.get('coordinates')[0]

# Derive bounding box from two corners of the bounding rectangle
bbox = [bounding_coords[0][0], bounding_coords[0][1], bounding_coords[2][0], bounding_coords[2][1]]

# Get centroid
centroid = fc.geometry().centroid().getInfo()

# Compute length diagonal of bbox for zoom calculation
diag = math.sqrt((bbox[0] - bbox[2])**2 + (bbox[1] - bbox[3])**2)

# Found the diagonal length and zoom level for US and Kenya boundaries
# Used equation of a line to develop the relationship between zoom and diagonal of bounding box
zoom = round((-0.0701 * diag) + 8.34, 0)

# The returned ee.FeatureClass properties
fc_props = {
'zoom': zoom,
'bbox': bbox,
'centroid': centroid.get('coordinates')
}

return fc_props

0 comments on commit e07e34a

Please sign in to comment.