Skip to content

Commit

Permalink
Initial File Upload Solution
Browse files Browse the repository at this point in the history
  • Loading branch information
swainn committed May 7, 2020
1 parent f96bfbf commit 95c3eaf
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 4 deletions.
1 change: 1 addition & 0 deletions install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ requirements:
- earthengine-api
- oauth2client
- geojson
- pyshp
pip:

post:
89 changes: 86 additions & 3 deletions tethysapp/earth_engine/controllers.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import os
import tempfile
import zipfile
import logging
import datetime as dt
import geojson
from django.http import JsonResponse, HttpResponseNotAllowed
import shapefile
from django.http import JsonResponse, HttpResponseNotAllowed, HttpResponseRedirect
from django.shortcuts import render
from simplejson.errors import JSONDecodeError
from tethys_sdk.gizmos import SelectInput, DatePicker, Button, MapView, MVView, PlotlyView, MVDraw
from tethys_sdk.permissions import login_required
from .helpers import generate_figure
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.products import EE_PRODUCTS

Expand All @@ -32,7 +37,8 @@ def about(request):


@login_required()
def viewer(request):
@user_workspace
def viewer(request, user_workspace):
"""
Controller for the app viewer page.
"""
Expand Down Expand Up @@ -187,6 +193,27 @@ def viewer(request):
)
)

# Boundary Upload Form
set_boundary_button = Button(
name='set_boundary',
display_text='Set Boundary',
style='default',
attributes={
'id': 'set_boundary',
'data-toggle': 'modal',
'data-target': '#set-boundary-modal' # ID of the Set Boundary Modal
}
)

# Handle Set Boundary Form
set_boundary_error = ''
if request.POST and request.FILES:
set_boundary_error = handle_shapefile_upload(request, user_workspace)

if not set_boundary_error:
# Redirect back to this page to clear form
return HttpResponseRedirect(request.path)

context = {
'platform_select': platform_select,
'sensor_select': sensor_select,
Expand All @@ -197,6 +224,8 @@ def viewer(request):
'load_button': load_button,
'clear_button': clear_button,
'plot_button': plot_button,
'set_boundary_button': set_boundary_button,
'set_boundary_error': set_boundary_error,
'ee_products': EE_PRODUCTS,
'map_view': map_view
}
Expand Down Expand Up @@ -316,3 +345,57 @@ def get_time_series_plot(request):
log.exception('An unexpected error occurred.')

return render(request, 'earth_engine/plot.html', context)


def handle_shapefile_upload(request, user_workspace):
"""
Uploads shapefile to Google Earth Engine as an Asset.
Args:
request (django.Request): the request object.
user_workspace (tethys_sdk.workspaces.Workspace): the User workspace object.
Returns:
str: Error string if errors occurred.
"""
# Write file to temp for processing
uploaded_file = request.FILES['boundary-file']

with tempfile.TemporaryDirectory() as temp_dir:
temp_zip_path = os.path.join(temp_dir, 'boundary.zip')

# Use with statements to ensure opened files are closed when done
with open(temp_zip_path, 'wb') as temp_zip:
for chunk in uploaded_file.chunks():
temp_zip.write(chunk)

try:
# Extract the archive to the temporary directory
with zipfile.ZipFile(temp_zip_path) as temp_zip:
temp_zip.extractall(temp_dir)

except zipfile.BadZipFile:
# Return error message
return 'You must provide a zip archive containing a shapefile.'

# Verify that it contains a shapefile
try:
# Find a shapefile in directory where we extracted the archive
shapefile_path = find_shapefile(temp_dir)

if not shapefile_path:
return 'No Shapefile found in the archive provided.'

with shapefile.Reader(shapefile_path) as shp_file:
# Check type (only Polygon supported)
if shp_file.shapeType != shapefile.POLYGON:
return 'Only shapefiles containing Polygons are supported.'

# Setup workspace directory for storing shapefile
workspace_dir = prep_boundary_dir(user_workspace.path)

# Write the shapefile to the workspace directory
write_boundary_shapefile(shp_file, workspace_dir)

except TypeError:
return 'Incomplete or corrupted shapefile provided.'
84 changes: 84 additions & 0 deletions tethysapp/earth_engine/helpers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import os
import glob
import shapefile
import pandas as pd
from plotly import graph_objs as go

Expand Down Expand Up @@ -49,3 +52,84 @@ def generate_figure(figure_title, time_series):
}

return figure


def find_shapefile(directory):
"""
Recursively find the path to the first file with an extension ".shp" in the given directory.
Args:
directory (str): Path of directory to search for shapefile.
Returns:
str: Path to first shapefile found in given directory.
"""
shapefile_path = ''

# Scan the temp directory using walk, searching for a shapefile (.shp extension)
for root, dirs, files in os.walk(directory):
for f in files:
f_path = os.path.join(root, f)
f_ext = os.path.splitext(f_path)[1]

if f_ext == '.shp':
shapefile_path = f_path
break

return shapefile_path


def prep_boundary_dir(root_path):
"""
Setup the workspace directory that will store the uploaded boundary shapefile.
Args:
root_path (str): path to the root directory where the boundary directory will be located.
Returns:
str: path to boundary directory for storing boundary shapefile.
"""
# Copy into new shapefile in user directory
boundary_dir = os.path.join(root_path, 'boundary')

# Make the directory if it doesn't exist
if not os.path.isdir(boundary_dir):
os.mkdir(boundary_dir)

# Clear the directory if it exists
else:
# Find all files in the directory using glob
files = glob.glob(os.path.join(boundary_dir, '*'))

# Remove all the files
for f in files:
os.remove(f)

return boundary_dir


def write_boundary_shapefile(shp_file, directory):
"""
Write the shapefile to the given directory. The shapefile will be called "boundary.shp".
Args:
shp_file (shapefile.Reader): A shapefile reader object.
directory (str): Path to directory to which to write shapefile.
Returns:
str: path to shapefile that was written.
"""
# Name the shapefiles boundary.* (boundary.shp, boundary.dbf, boundary.shx)
shapefile_path = os.path.join(directory, 'boundary')

# Write contents of shapefile to new shapfile
with shapefile.Writer(shapefile_path) as out_shp:
# Based on https://pypi.org/project/pyshp/#examples
out_shp.fields = shp_file.fields[1:] # skip the deletion field

# Add the existing shape objects
for shaperec in shp_file.iterShapeRecords():
out_shp.record(*shaperec.record)
out_shp.shape(shaperec.shape)

return shapefile_path
7 changes: 6 additions & 1 deletion tethysapp/earth_engine/public/js/gee_datasets.js
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ var GEE_DATASETS = (function() {
*************************************************************************/
public_interface = {};

/************************************************************************
/************************************************************************
* INITIALIZATION / CONSTRUCTOR
*************************************************************************/
$(function() {
Expand All @@ -328,6 +328,11 @@ var GEE_DATASETS = (function() {
m_reducer = $('#reducer').val();

m_map = TETHYS_MAP_VIEW.getMap();

// Open boundary file modal if it has an error
if ($('#boundary-file-form-group').hasClass('has-error')) {
$('#set-boundary-modal').modal('show');
}
});

return public_interface;
Expand Down
32 changes: 32 additions & 0 deletions tethysapp/earth_engine/templates/earth_engine/viewer.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
{% gizmo clear_button %}
<p class="help">Draw an area of interest or drop a point, the press "Plot AOI" to view a plot of the data.</p>
{% gizmo plot_button %}
<p class="help">Upload a shapefile of a boundary to use to clip datasets and set the default extent.</p>
{% gizmo set_boundary_button %}
{% endblock %}

{% block app_content %}
Expand All @@ -49,6 +51,36 @@ <h5 class="modal-title" id="plot-modal-label">Area of Interest Plot</h5>
</div>
</div>
<!-- End Plot Modal -->
<!-- Set Boundary Modal -->
<div class="modal fade" id="set-boundary-modal" tabindex="-1" role="dialog" aria-labelledby="set-boundary-modal-label">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h5 class="modal-title" id="set-boundary-modal-label">Set Boundary</h5>
</div>
<div class="modal-body">
<form class="horizontal-form" id="set-boundary-form" method="post" action="" enctype="multipart/form-data">
<p>Create a zip archive containing a shapefile and supporting files (i.e.: .shp, .shx, .dbf). Then use the file browser button below to select it.</p>
<!-- This is required for POST method -->
{% csrf_token %}
<div id="boundary-file-form-group" class="form-group{% if set_boundary_error %} has-error{% endif %}">
<label class="control-label" for="boundary-file">Boundary Shapefile</label>
<input type="file" name="boundary-file" id="boundary-file" accept="zip">
{% if set_boundary_error %}
<p class="help-block">{{ set_boundary_error }}</p>
{% endif %}
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<input type="submit" class="btn btn-default" value="Set Boundary" name="set-boundary-submit" id="set-boundary-submit" form="set-boundary-form">
</div>
</div>
</div>
</div>
<!-- End Set Boundary Modal -->
<div id="ee-products" data-ee-products="{{ ee_products|jsonify }}"></div>
<div id="loader">
<img src="{% static 'earth_engine/images/map-loader.gif' %}">
Expand Down

0 comments on commit 95c3eaf

Please sign in to comment.