# notebookapp_miningrehab.py
'''
This file contains functions for loading and interacting with data in the
mining rehabilitation notebook, inside the Real_world_examples folder.
Available functions:
load_miningrehab_data
run_miningrehab_app
Last modified: September 2021
'''
# Load modules
from ipyleaflet import Map, GeoJSON, DrawControl, basemaps
import datetime as dt
import datacube
import geopandas as gpd
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
import rasterio.features
import IPython
from IPython.display import display
import warnings
import ipywidgets as widgets
from datacube.utils import masking
# Load utility functions
import sys
sys.path.insert(1, '../Tools/')
from dea_tools.datahandling import wofs_fuser
from dea_tools.spatial import transform_geojson_wgs_to_epsg
[docs]
def load_miningrehab_data():
"""
Loads DEA Fractional Cover and Water Observations products for the mining
case-study area.
Last modified: July 2021
outputs
ds - data set containing masked Fractional Cover data from Landsat 8
Masked values are set to 'nan'
"""
# Suppress warnings
warnings.filterwarnings("ignore")
# Initialise the data cube. 'app' argument is used to identify this app
dc = datacube.Datacube(app="mining-app")
# Specify latitude and longitude ranges
latitude = (-34.426512, -34.434517)
longitude = (116.648123, 116.630731)
# Specify the date range
time = ("2015-06-01", "2018-06-30")
# Construct the data cube query
query = {
"x": longitude,
"y": latitude,
"time": time,
"output_crs": "EPSG:3577",
"resolution": (-30, 30),
}
print("Loading DEA Fractional Cover")
dataset_fc = dc.load(product="ga_ls_fc_3",
group_by='solar_day',
platform='landsat-8',
cloud_cover= (0, 10),
**query)
print("Loading DEA Water Observations")
dataset_wo = dc.load(product="ga_ls_wo_3",
group_by='solar_day',
platform='landsat-8',
fuse_func=wofs_fuser,
cloud_cover= (0, 10),
like=dataset_fc)
# Match the data
shared_times = np.intersect1d(dataset_fc.time, dataset_wo.time)
ds_fc_matched = dataset_fc.sel(time=shared_times)
ds_wo_matched = dataset_wo.sel(time=shared_times)
# Mask FC
dry_mask = masking.make_mask(ds_wo_matched, dry=True)
# Get fractional masked fc dataset (as proportion of 1, rather than 100)
ds_fc_masked = ds_fc_matched.where(dry_mask.water) / 100
# Resample
ds_resampled = ds_fc_masked #.resample(time="3M").median()
ds_resampled.attrs["crs"] = dataset_fc.crs
# Return the data
return ds_fc_masked
[docs]
def run_miningrehab_app(ds):
"""
Plots an interactive map of the mining case-study area and allows
the user to draw polygons. This returns plots of the fractional cover value
of bare soil, green vegetation and brown vegetation in the polygon area.
Last modified: January 2020
inputs
ds - data set containing masked Fractional Cover data from Landsat 8
"""
# Suppress warnings
warnings.filterwarnings("ignore")
# Update plotting functionality through rcParams
mpl.rcParams.update({"figure.autolayout": True})
# Define the bounding box that will be overlayed on the interactive map
# The bounds are hard-coded to match those from the loaded data
geom_obj = {
"type": "Feature",
"properties": {
"style": {
"stroke": True,
"color": "red",
"weight": 4,
"opacity": 0.8,
"fill": True,
"fillColor": False,
"fillOpacity": 0,
"showArea": True,
"clickable": True,
}
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[116.630731, -34.434517],
[116.630731, -34.426512],
[116.648123, -34.426512],
[116.648123, -34.434517],
[116.630731, -34.434517],
]
],
},
}
# Create a map geometry from the geom_obj dictionary
# center specifies where the background map view should focus on
# zoom specifies how zoomed in the background map should be
centroid = gpd.GeoDataFrame.from_features([geom_obj]).geometry.centroid
loadeddata_center = centroid.y.item(), centroid.x.item()
loadeddata_zoom = 15
# define the study area map
studyarea_map = Map(
layout=widgets.Layout(width="480px", height="600px"),
center=loadeddata_center,
zoom=loadeddata_zoom,
basemap=basemaps.Esri.WorldImagery,
)
# define the drawing controls
studyarea_drawctrl = DrawControl(
polygon={"shapeOptions": {"fillOpacity": 0}},
marker={},
circle={},
circlemarker={},
polyline={},
)
# add drawing controls and data bound geometry to the map
studyarea_map.add_control(studyarea_drawctrl)
studyarea_map.add_layer(GeoJSON(data=geom_obj))
# Index to count drawn polygons
polygon_number = 0
# Define widgets to interact with
instruction = widgets.Output(layout={"border": "1px solid black"})
with instruction:
print(
"Draw a polygon within the red box to view plots of "
"the fractional cover values of bare, green and "
"non-green cover for the area over time."
)
info = widgets.Output(layout={"border": "1px solid black"})
with info:
print("Plot status:")
fig_display = widgets.Output(
layout=widgets.Layout(
width="50%" # proportion of horizontal space taken by plot
)
)
with fig_display:
plt.ioff()
fig, ax = plt.subplots(3, 1, figsize=(9, 9))
for axis in ax:
axis.set_ylim([0, 1])
colour_list = plt.rcParams["axes.prop_cycle"].by_key()["color"]
# Function to execute each time something is drawn on the map
def handle_draw(self, action, geo_json):
nonlocal polygon_number
# info.clear_output(wait=True) # wait=True reduces flicker effect
# with info:
# print("Plot status: entered handle draw")
# Execute behaviour based on what the user draws
if geo_json["geometry"]["type"] == "Polygon":
# Convert the drawn geometry to pixel coordinates
geom_selectedarea = transform_geojson_wgs_to_epsg(
geo_json,
EPSG=3577 # hard-coded to be same as case-study data
)
# Construct a mask to only select pixels within the drawn polygon
mask = rasterio.features.geometry_mask(
[geom_selectedarea for geoms in [geom_selectedarea]],
out_shape=ds.geobox.shape,
transform=ds.geobox.affine,
all_touched=False,
invert=True,
)
masked_ds = ds.where(mask)
masked_ds_mean = masked_ds.mean(dim=["x", "y"], skipna=True)
colour = colour_list[polygon_number % len(colour_list)]
# Add a layer to the map to make the most recently drawn polygon
# the same colour as the line on the plot
studyarea_map.add_layer(
GeoJSON(
data=geo_json,
style={
"color": colour,
"opacity": 1,
"weight": 4.5,
"fillOpacity": 0.0,
},
)
)
# Add Fractional cover plots to app
masked_ds_mean = masked_ds_mean.drop('spatial_ref')
masked_ds_mean.bs.interpolate_na(dim="time", method="nearest").plot.line("-", ax=ax[0])
masked_ds_mean.pv.interpolate_na(dim="time", method="nearest").plot.line("-", ax=ax[1])
masked_ds_mean.npv.interpolate_na(dim="time", method="nearest").plot.line("-", ax=ax[2])
# reset titles back to custom
ax[0].set_ylabel("Bare soil")
ax[1].set_ylabel("Green vegetation")
ax[2].set_ylabel("Brown (i.e. non-green) vegetation")
ax[0].set_xlabel("")
ax[1].set_xlabel("")
ax[2].set_xlabel(None)
# refresh display
fig_display.clear_output(wait=True) # wait=True reduces flicker effect
with fig_display:
display(fig)
# Update plot info
info.clear_output(wait=True) # wait=True reduces flicker effect
with info:
print("Plot status: polygon added to plot")
# Iterate the polygon number before drawing another polygon
polygon_number = polygon_number + 1
else:
info.clear_output(wait=True)
with info:
print(
"Plot status: this drawing tool is not currently "
"supported. Please use the polygon tool."
)
# call to say activate handle_draw function on draw
studyarea_drawctrl.on_draw(handle_draw)
with fig_display:
# TODO: update with user friendly something
display(widgets.HTML(""))
# Construct UI:
# +-----------------------+
# | instruction |
# +-----------+-----------+
# | map | plot |
# | | |
# +-----------+-----------+
# | info |
# +-----------------------+
ui = widgets.VBox([instruction, widgets.HBox([studyarea_map, fig_display]), info])
display(ui)