first commit
This commit is contained in:
@@ -0,0 +1,942 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Satellite imagery animation widget, which can be used to interactively
|
||||
produce animations for multiple DE Africa products.
|
||||
"""
|
||||
|
||||
# Import required packages
|
||||
|
||||
# Force GeoPandas to use Shapely instead of PyGEOS
|
||||
# In a future release, GeoPandas will switch to using Shapely by default.
|
||||
import os
|
||||
os.environ['USE_PYGEOS'] = '0'
|
||||
|
||||
import fiona
|
||||
import sys
|
||||
import datacube
|
||||
import warnings
|
||||
import matplotlib.pyplot as plt
|
||||
from datacube.utils.geometry import CRS
|
||||
from ipyleaflet import (
|
||||
WMSLayer,
|
||||
basemaps,
|
||||
basemap_to_tiles,
|
||||
Map,
|
||||
DrawControl,
|
||||
WidgetControl,
|
||||
LayerGroup,
|
||||
LayersControl,
|
||||
GeoData,
|
||||
)
|
||||
from traitlets import Unicode
|
||||
from ipywidgets import (
|
||||
GridspecLayout,
|
||||
Button,
|
||||
Layout,
|
||||
HBox,
|
||||
VBox,
|
||||
HTML,
|
||||
Output,
|
||||
)
|
||||
import json
|
||||
import itertools
|
||||
import numpy as np
|
||||
import geopandas as gpd
|
||||
from io import BytesIO
|
||||
import ipywidgets as widgets
|
||||
import datetime
|
||||
from skimage import exposure
|
||||
from skimage.filters import unsharp_mask
|
||||
|
||||
from datacube.utils import masking
|
||||
from datacube.utils.geometry import Geometry
|
||||
from datacube.utils.masking import mask_invalid_data
|
||||
import deafrica_tools.app.widgetconstructors as deawidgets
|
||||
from deafrica_tools.dask import create_local_dask_cluster
|
||||
from deafrica_tools.spatial import reverse_geocode
|
||||
from deafrica_tools.datahandling import pan_sharpen_brovey
|
||||
|
||||
import warnings
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
|
||||
# WMS params and satellite style bands
|
||||
sat_params = {
|
||||
"Landsat": {
|
||||
"products": ["ls5_sr", "ls7_sr", "ls8_sr", "ls9_sr"],
|
||||
"styles": {
|
||||
"True colour": ("true_colour", ["red", "green", "blue"]),
|
||||
"False colour": (
|
||||
"false_colour",
|
||||
["swir_1", "nir", "green"],
|
||||
),
|
||||
},
|
||||
},
|
||||
"Sentinel-2": {
|
||||
"products": ["s2_l2a"],
|
||||
"styles": {
|
||||
"True colour": ("simple_rgb", ["red", "green", "blue"]),
|
||||
"False colour": (
|
||||
"infrared_green",
|
||||
["swir_2", "nir_1", "green"],
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def make_box_layout():
|
||||
return Layout(
|
||||
# border='solid 1px black',
|
||||
margin="0px 10px 10px 0px",
|
||||
padding="5px 5px 5px 5px",
|
||||
width="100%",
|
||||
height="100%",
|
||||
)
|
||||
|
||||
|
||||
def create_expanded_button(description, button_style):
|
||||
return Button(
|
||||
description=description,
|
||||
button_style=button_style,
|
||||
layout=Layout(width="auto", height="auto"),
|
||||
)
|
||||
|
||||
|
||||
def update_map_layers(self):
|
||||
"""
|
||||
Updates map to add new DE Africa layers, styles or basemap when selected
|
||||
using menu options. Triggers data reload by resetting load params
|
||||
and output arrays.
|
||||
"""
|
||||
|
||||
# Clear data load params to trigger data re-load
|
||||
self.timeseries_ds = None
|
||||
self.load_params = None
|
||||
self.query_params = None
|
||||
|
||||
# Clear all layers and add basemap
|
||||
self.map_layers.clear_layers()
|
||||
self.map_layers.add_layer(self.basemap)
|
||||
|
||||
|
||||
def extract_data(self):
|
||||
|
||||
# Connect to datacube database
|
||||
dc = datacube.Datacube(app="Exporting satellite images")
|
||||
|
||||
# Configure local dask cluster
|
||||
client = create_local_dask_cluster(return_client=True, display_client=True)
|
||||
|
||||
# Convert to geopolygon
|
||||
geopolygon = Geometry(geom=self.gdf_drawn.geometry[0], crs=self.gdf_drawn.crs)
|
||||
|
||||
# Create query.
|
||||
start_date = np.datetime64(self.start_date)
|
||||
end_date = np.datetime64(self.end_date)
|
||||
|
||||
self.query_params = {
|
||||
"time": (str(start_date), str(end_date)),
|
||||
"geopolygon": geopolygon,
|
||||
}
|
||||
|
||||
# Find matching datasets
|
||||
dss = [
|
||||
dc.find_datasets(product=i, **self.query_params)
|
||||
for i in sat_params[self.dealayer]["products"]
|
||||
]
|
||||
dss = list(itertools.chain.from_iterable(dss))
|
||||
|
||||
# If data is found
|
||||
if len(dss) > 0:
|
||||
|
||||
# Get CRS
|
||||
crs = str(dss[0].crs)
|
||||
|
||||
self.load_params = {
|
||||
"measurements": sat_params[self.dealayer]["styles"][self.style][1],
|
||||
"resolution": (-self.resolution, self.resolution),
|
||||
"output_crs": crs,
|
||||
"group_by": "solar_day",
|
||||
"dask_chunks": {"time": 1, "x": 2048, "y": 2048},
|
||||
"resampling": {"*": "cubic", "oa_fmask": "nearest", "fmask": "nearest"},
|
||||
}
|
||||
|
||||
# Load data
|
||||
from deafrica_tools.datahandling import load_ard
|
||||
|
||||
timeseries_ds = load_ard(
|
||||
dc=dc,
|
||||
products=sat_params[self.dealayer]["products"],
|
||||
min_gooddata=1.0 - (self.max_cloud_cover / 100),
|
||||
ls7_slc_off=False,
|
||||
mask_pixel_quality=self.cloud_mask,
|
||||
**self.load_params,
|
||||
**self.query_params,
|
||||
)
|
||||
|
||||
# Set invalid nodata pixels to NaN
|
||||
timeseries_ds = mask_invalid_data(timeseries_ds)
|
||||
|
||||
# Else if no data is returned, return None
|
||||
else:
|
||||
timeseries_ds = None
|
||||
|
||||
# Close down the dask client
|
||||
client.close()
|
||||
|
||||
return timeseries_ds.compute()
|
||||
|
||||
|
||||
def plot_data(self, fname):
|
||||
|
||||
# Data to plot
|
||||
to_plot = self.timeseries_ds
|
||||
|
||||
# If rolling median specified
|
||||
if self.rolling_median:
|
||||
with self.status_info:
|
||||
print(
|
||||
f"\nApplying rolling median ({self.rolling_median_window} timesteps window)"
|
||||
)
|
||||
to_plot = to_plot.rolling(
|
||||
time=int(self.rolling_median_window), center=True, min_periods=1
|
||||
).median()
|
||||
|
||||
# If resampling freq specified
|
||||
if self.resample_freq:
|
||||
with self.status_info:
|
||||
print(f"\nResampling data to {self.resample_freq} frequency")
|
||||
to_plot = to_plot.resample(time=self.resample_freq).median()
|
||||
|
||||
# Raise by power to dampen bright features and enhance dark.
|
||||
# Raise vmin and vmax by same amount to ensure proper stretch
|
||||
if self.power < 1.0:
|
||||
with self.status_info:
|
||||
print(f"\nApplying power transformation ({self.power})")
|
||||
to_plot = to_plot ** self.power
|
||||
|
||||
# Apply unsharp masking to enhance overall dynamic range,
|
||||
# and improve fine scale detail
|
||||
if self.unsharp_mask:
|
||||
with self.status_info:
|
||||
print(
|
||||
f"\nApplying unsharp masking with {self.unsharp_mask_radius} "
|
||||
f"radius and {self.unsharp_mask_amount} amount"
|
||||
)
|
||||
from skimage.exposure import rescale_intensity
|
||||
|
||||
funcs_list = [
|
||||
rescale_intensity,
|
||||
lambda x: unsharp_mask(
|
||||
x, radius=self.unsharp_mask_radius, amount=self.unsharp_mask_amount
|
||||
),
|
||||
]
|
||||
else:
|
||||
funcs_list = None
|
||||
|
||||
from deafrica_tools.plotting import xr_animation
|
||||
|
||||
xr_animation(
|
||||
output_path=fname,
|
||||
ds=to_plot.dropna(dim="time", how="all"),
|
||||
show_text="",
|
||||
bands=sat_params[self.dealayer]["styles"][self.style][1],
|
||||
interval=self.interval,
|
||||
width_pixels=self.width,
|
||||
show_gdf=deacoastlines_overlay(to_plot) if self.deacoastlines else None,
|
||||
gdf_kwargs={"linewidth": 3},
|
||||
percentile_stretch=(self.vmin, self.vmax),
|
||||
image_proc_funcs=funcs_list,
|
||||
show_date="%Y" if self.resample_freq == "1Y" else "%b %Y",
|
||||
annotation_kwargs={"fontsize": 75},
|
||||
)
|
||||
|
||||
# Add plot preview below map and finish
|
||||
plt.show()
|
||||
with self.status_info:
|
||||
print(f"\nImage successfully exported to:\n{fname}.")
|
||||
|
||||
|
||||
def deacoastlines_overlay(ds):
|
||||
|
||||
import geopandas as gpd
|
||||
import pandas as pd
|
||||
import matplotlib
|
||||
from shapely.geometry import box, Point
|
||||
from deafrica_tools.coastal import get_coastlines
|
||||
|
||||
# Get bounding box of data
|
||||
xmin, ymin, xmax, ymax = ds.geobox.geographic_extent.boundingbox
|
||||
bounds = [xmin, ymin, xmax, ymax]
|
||||
|
||||
# Load data
|
||||
deacl_gdf = get_coastlines(bbox=bounds)
|
||||
|
||||
# Clip to extent of satellite data
|
||||
bbox = gpd.GeoDataFrame(geometry=[ds.geobox.extent.geom], crs=ds.geobox.crs)
|
||||
deacl_gdf = gpd.overlay(deacl_gdf, bbox.to_crs(deacl_gdf.crs))
|
||||
deacl_gdf = deacl_gdf.dissolve("year") # values("year", ascending=True)
|
||||
|
||||
# Apply colours
|
||||
norm = matplotlib.colors.Normalize(vmin=0, vmax=len(deacl_gdf.index))
|
||||
cmap = matplotlib.cm.get_cmap("inferno")
|
||||
rgba = cmap(norm(deacl_gdf.reset_index().index))
|
||||
deacl_gdf["color"] = list(rgba)
|
||||
deacl_gdf["start_time"] = pd.to_datetime(deacl_gdf.index) + pd.DateOffset(months=0)
|
||||
deacl_gdf = deacl_gdf.sort_index()
|
||||
|
||||
if len(deacl_gdf.index) > 0:
|
||||
return deacl_gdf
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class animation_app(HBox):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
######################
|
||||
# INITIAL ATTRIBUTES #
|
||||
######################
|
||||
|
||||
# Basemap
|
||||
self.basemap_list = [
|
||||
("ESRI World Imagery", basemap_to_tiles(basemaps.Esri.WorldImagery)),
|
||||
("Open Street Map", basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)),
|
||||
]
|
||||
self.basemap = self.basemap_list[0][1]
|
||||
|
||||
# Satellite data
|
||||
end_date = datetime.datetime.today()
|
||||
start_date = datetime.datetime(
|
||||
year=end_date.year - 3, month=end_date.month, day=end_date.day
|
||||
)
|
||||
self.start_date = start_date.strftime("%Y-%m-%d")
|
||||
self.end_date = end_date.strftime("%Y-%m-%d")
|
||||
self.dealayer_list = [
|
||||
("Landsat", "Landsat"),
|
||||
("Sentinel-2", "Sentinel-2"),
|
||||
]
|
||||
self.dealayer = self.dealayer_list[0][1]
|
||||
|
||||
# Styles
|
||||
self.styles_list = ["True colour", "False colour"]
|
||||
self.style = self.styles_list[0]
|
||||
|
||||
# Analysis params
|
||||
self.resolution = 30
|
||||
self.vmin = 0.01
|
||||
self.vmax = 0.99
|
||||
self.power = 1.0
|
||||
self.output_list = [("MP4", "mp4"), ("GIF", "gif")]
|
||||
self.output_format = self.output_list[0][1]
|
||||
self.rolling_median = False
|
||||
self.rolling_median_window = 20
|
||||
self.unsharp_mask = False
|
||||
self.unsharp_mask_radius = 20
|
||||
self.unsharp_mask_amount = 0.3
|
||||
self.max_size = False
|
||||
self.width = 900
|
||||
self.interval = 100
|
||||
self.cloud_mask = False
|
||||
self.max_cloud_cover = 20
|
||||
self.resample_list = [
|
||||
("None", False),
|
||||
("Monthly", "1M"),
|
||||
("Quarterly", "Q-DEC"),
|
||||
("Yearly", "1Y"),
|
||||
]
|
||||
self.resample_freq = self.resample_list[0][1]
|
||||
self.deacoastlines = False
|
||||
|
||||
# Drawing params
|
||||
self.target = None
|
||||
self.action = None
|
||||
self.gdf_drawn = None
|
||||
|
||||
# Data load params
|
||||
self.timeseries_ds = None
|
||||
self.load_params = None
|
||||
self.query_params = None
|
||||
|
||||
##################
|
||||
# HEADER FOR APP #
|
||||
##################
|
||||
|
||||
# Create the Header widget
|
||||
header_title_text = (
|
||||
"<h3>Digital Earth Africa satellite imagery animations</h3>"
|
||||
)
|
||||
instruction_text = (
|
||||
"<p>Select the desired satellite data, imagery date range "
|
||||
"and image style, then zoom in and draw a rectangle to "
|
||||
"select an area export as a satellite imagery time-series "
|
||||
"animation.</p>"
|
||||
)
|
||||
self.header = deawidgets.create_html(f"{header_title_text}{instruction_text}")
|
||||
self.header.layout = make_box_layout()
|
||||
|
||||
#####################################
|
||||
# HANDLER FUNCTION FOR DRAW CONTROL #
|
||||
#####################################
|
||||
|
||||
# Define the action to take once something is drawn on the map
|
||||
def update_geojson(target, action, geo_json):
|
||||
|
||||
# Get data from action
|
||||
self.action = action
|
||||
|
||||
# Clear data load params to trigger data re-load
|
||||
self.timeseries_ds = None
|
||||
self.load_params = None
|
||||
self.query_params = None
|
||||
|
||||
# Convert data to geopandas
|
||||
json_data = json.dumps(geo_json)
|
||||
binary_data = json_data.encode()
|
||||
io = BytesIO(binary_data)
|
||||
io.seek(0)
|
||||
gdf = gpd.read_file(io)
|
||||
gdf.crs = "EPSG:4326"
|
||||
|
||||
# Convert to WGS 84 / NSIDC EASE-Grid 2.0 Global and compute area
|
||||
gdf_drawn_nsidc = gdf.copy().to_crs("EPSG:6933")
|
||||
m2_per_ha = 10000
|
||||
area = gdf_drawn_nsidc.area.values[0] / m2_per_ha
|
||||
polyarea_label = "Total area of satellite data to extract"
|
||||
polyarea_text = f"<b>{polyarea_label}</b>: {area:.2f} ha</sup>"
|
||||
|
||||
# Test area size
|
||||
if self.max_size:
|
||||
confirmation_text = (
|
||||
'<span style="color: #33cc33"> '
|
||||
"<b>(Overriding maximum size limit; use with caution as may lead to memory issues)</b></span>"
|
||||
)
|
||||
self.header.value = (
|
||||
header_title_text
|
||||
+ instruction_text
|
||||
+ polyarea_text
|
||||
+ confirmation_text
|
||||
)
|
||||
self.gdf_drawn = gdf
|
||||
elif area <= 50000:
|
||||
confirmation_text = (
|
||||
'<span style="color: #33cc33"> '
|
||||
"<b>(Area to extract falls within "
|
||||
"recommended 50000 ha limit)</b></span>"
|
||||
)
|
||||
self.header.value = (
|
||||
header_title_text
|
||||
+ instruction_text
|
||||
+ polyarea_text
|
||||
+ confirmation_text
|
||||
)
|
||||
self.gdf_drawn = gdf
|
||||
else:
|
||||
warning_text = (
|
||||
'<span style="color: #ff5050"> '
|
||||
"<b>(Area to extract is too large, "
|
||||
"please select an area less than 50000 "
|
||||
"ha)</b></span>"
|
||||
)
|
||||
self.header.value = (
|
||||
header_title_text + instruction_text + polyarea_text + warning_text
|
||||
)
|
||||
self.gdf_drawn = None
|
||||
|
||||
###########################
|
||||
# WIDGETS FOR APP OUTPUTS #
|
||||
###########################
|
||||
|
||||
self.status_info = Output(layout=make_box_layout())
|
||||
self.output_plot = Output(layout=make_box_layout())
|
||||
|
||||
#########################################
|
||||
# MAP WIDGET, DRAWING TOOLS, WMS LAYERS #
|
||||
#########################################
|
||||
|
||||
# Create drawing tools
|
||||
desired_drawtools = ["rectangle"]
|
||||
draw_control = deawidgets.create_drawcontrol(desired_drawtools)
|
||||
|
||||
# Begin by displaying an empty layer group, and update the group with desired WMS on interaction.
|
||||
self.map_layers = LayerGroup(layers=())
|
||||
self.map_layers.name = "Map Overlays"
|
||||
|
||||
# Create map widget
|
||||
self.m = deawidgets.create_map(map_center=(5.65, 26.17), zoom_level=13)
|
||||
self.m.layout = make_box_layout()
|
||||
|
||||
# Add tools to map widget
|
||||
self.m.add_control(draw_control)
|
||||
self.m.add_layer(self.map_layers)
|
||||
|
||||
# Update all maps to starting defaults
|
||||
update_map_layers(self)
|
||||
|
||||
############################
|
||||
# WIDGETS FOR APP CONTROLS #
|
||||
############################
|
||||
|
||||
# Create parameter widgets
|
||||
dropdown_basemap = deawidgets.create_dropdown(
|
||||
self.basemap_list, self.basemap_list[0][1]
|
||||
)
|
||||
dropdown_dealayer = deawidgets.create_dropdown(
|
||||
self.dealayer_list, self.dealayer_list[0][1]
|
||||
)
|
||||
dropdown_output = deawidgets.create_dropdown(
|
||||
self.output_list, self.output_list[0][1]
|
||||
)
|
||||
date_picker_start = deawidgets.create_datepicker(
|
||||
value=start_date,
|
||||
)
|
||||
date_picker_end = deawidgets.create_datepicker(
|
||||
value=end_date,
|
||||
)
|
||||
dropdown_styles = deawidgets.create_dropdown(
|
||||
self.styles_list, self.styles_list[0]
|
||||
)
|
||||
slider_percentile = widgets.FloatRangeSlider(
|
||||
value=[0.01, 0.99],
|
||||
min=0,
|
||||
max=1,
|
||||
step=0.001,
|
||||
description="",
|
||||
layout={"width": "85%"},
|
||||
)
|
||||
run_button = create_expanded_button("Generate animation", "info")
|
||||
|
||||
floatslider_max_cloud_cover = widgets.IntSlider(
|
||||
value=20,
|
||||
min=0,
|
||||
max=100,
|
||||
step=1,
|
||||
description="",
|
||||
layout={"width": "85%"},
|
||||
)
|
||||
|
||||
checkbox_rolling_median = deawidgets.create_checkbox(
|
||||
self.rolling_median,
|
||||
"Apply rolling median<br>to produce smooth, <br> cloud-free animations",
|
||||
layout={"width": "90%",
|
||||
"height": "4em"},
|
||||
)
|
||||
text_rolling_median_window = widgets.IntText(
|
||||
value=20,
|
||||
step=1,
|
||||
description="</br>Rolling window (timesteps)",
|
||||
layout={
|
||||
"width": "85%",
|
||||
"margin": "0px",
|
||||
"padding": "0px",
|
||||
"display": "none",
|
||||
},
|
||||
)
|
||||
|
||||
# Expandable advanced section
|
||||
text_interval = widgets.IntText(
|
||||
value=100, description="", step=50, layout={"width": "95%"}
|
||||
)
|
||||
text_resolution = widgets.FloatText(
|
||||
value=30,
|
||||
description="",
|
||||
layout={"width": "95%", "margin": "0px", "padding": "0px"},
|
||||
)
|
||||
text_width = widgets.IntText(
|
||||
value=900, description="", step=50, layout={"width": "95%"}
|
||||
)
|
||||
dropdown_resampling = deawidgets.create_dropdown(
|
||||
self.resample_list,
|
||||
self.resample_freq,
|
||||
description="",
|
||||
layout={"width": "95%"},
|
||||
)
|
||||
checkbox_cloud_mask = deawidgets.create_checkbox(
|
||||
self.cloud_mask, "Mask out cloudy <br> pixels", layout={"width": "95%", "height": "auto"}
|
||||
)
|
||||
slider_power = widgets.FloatSlider(
|
||||
value=1.0,
|
||||
min=0.01,
|
||||
max=1.0,
|
||||
step=0.01,
|
||||
description="",
|
||||
layout={"width": "95%"},
|
||||
)
|
||||
checkbox_unsharp_mask = deawidgets.create_checkbox(
|
||||
self.unsharp_mask, "Enable", layout={"width": "95%"}
|
||||
)
|
||||
text_unsharp_mask_radius = widgets.FloatText(
|
||||
value=20,
|
||||
step=1,
|
||||
description="Radius",
|
||||
layout={
|
||||
"width": "95%",
|
||||
"margin": "0px",
|
||||
"padding": "0px",
|
||||
"display": "none",
|
||||
},
|
||||
)
|
||||
text_unsharp_mask_amount = widgets.FloatText(
|
||||
value=0.3,
|
||||
step=0.1,
|
||||
description="Amount",
|
||||
layout={
|
||||
"width": "95%",
|
||||
"margin": "0px",
|
||||
"padding": "0px",
|
||||
"display": "none",
|
||||
},
|
||||
)
|
||||
checkbox_deacoastlines = deawidgets.create_checkbox(
|
||||
self.deacoastlines, "Add DE Africa Coastlines overlay", layout={"width": "95%"}
|
||||
)
|
||||
checkbox_max_size = deawidgets.create_checkbox(
|
||||
self.max_size, "Enable", layout={"width": "95%"}
|
||||
)
|
||||
expand_box = widgets.VBox(
|
||||
[
|
||||
HTML("Frame interval (milliseconds):"),
|
||||
text_interval,
|
||||
HTML("</br>Resolution (metres):"),
|
||||
text_resolution,
|
||||
HTML("</br>Width of output animation in pixels:"),
|
||||
text_width,
|
||||
HTML("</br>Apply temporal resampling:"),
|
||||
dropdown_resampling,
|
||||
HTML("</br>"),
|
||||
checkbox_cloud_mask,
|
||||
checkbox_deacoastlines,
|
||||
HTML("</br>Apply power transformation to darken bright features:"),
|
||||
slider_power,
|
||||
HTML("</br>Apply unsharp masking to sharpen imagery:"),
|
||||
checkbox_unsharp_mask,
|
||||
text_unsharp_mask_radius,
|
||||
text_unsharp_mask_amount,
|
||||
HTML(
|
||||
"</br>Override maximum size limit: (use with caution; may cause memory issues/crashes)"
|
||||
),
|
||||
checkbox_max_size,
|
||||
],
|
||||
)
|
||||
|
||||
expand = widgets.Accordion(
|
||||
children=[expand_box],
|
||||
selected_index=None,
|
||||
)
|
||||
expand.set_title(0, "Advanced")
|
||||
|
||||
# Add specific dialogs to class so they can be modified
|
||||
self.text_resolution = text_resolution
|
||||
self.text_unsharp_mask_radius = text_unsharp_mask_radius
|
||||
self.text_unsharp_mask_amount = text_unsharp_mask_amount
|
||||
self.text_rolling_median_window = text_rolling_median_window
|
||||
|
||||
####################################
|
||||
# UPDATE FUNCTIONS FOR EACH WIDGET #
|
||||
####################################
|
||||
|
||||
# Run update functions whenever various widgets are changed.
|
||||
date_picker_start.observe(self.update_start_date, "value")
|
||||
date_picker_end.observe(self.update_end_date, "value")
|
||||
dropdown_basemap.observe(self.update_basemap, "value")
|
||||
dropdown_dealayer.observe(self.update_dealayer, "value")
|
||||
dropdown_styles.observe(self.update_styles, "value")
|
||||
|
||||
slider_percentile.observe(self.update_slider_percentile, "value")
|
||||
floatslider_max_cloud_cover.observe(
|
||||
self.update_floatslider_max_cloud_cover, "value"
|
||||
)
|
||||
checkbox_rolling_median.observe(self.update_checkbox_rolling_median, "value")
|
||||
text_rolling_median_window.observe(
|
||||
self.update_text_rolling_median_window, "value"
|
||||
)
|
||||
dropdown_output.observe(self.update_output, "value")
|
||||
run_button.on_click(self.run_app)
|
||||
draw_control.on_draw(update_geojson)
|
||||
|
||||
# Advanced params
|
||||
text_resolution.observe(self.update_text_resolution, "value")
|
||||
slider_power.observe(self.update_slider_power, "value")
|
||||
text_width.observe(self.update_width, "value")
|
||||
text_interval.observe(self.update_interval, "value")
|
||||
dropdown_resampling.observe(self.update_dropdown_resampling, "value")
|
||||
checkbox_cloud_mask.observe(self.update_checkbox_cloud_mask, "value")
|
||||
checkbox_unsharp_mask.observe(self.update_checkbox_unsharp_mask, "value")
|
||||
text_unsharp_mask_radius.observe(self.update_text_unsharp_mask_radius, "value")
|
||||
text_unsharp_mask_amount.observe(self.update_text_unsharp_mask_amount, "value")
|
||||
checkbox_deacoastlines.observe(self.update_deacoastlines, "value")
|
||||
checkbox_max_size.observe(self.update_checkbox_max_size, "value")
|
||||
|
||||
##################################
|
||||
# COLLECTION OF ALL APP CONTROLS #
|
||||
##################################
|
||||
|
||||
parameter_selection = VBox(
|
||||
[
|
||||
HTML("<b>Satellite imagery:</b>"),
|
||||
dropdown_dealayer,
|
||||
HTML("<b>Start date:</b>"),
|
||||
date_picker_start,
|
||||
HTML("<b>End date:</b>"),
|
||||
date_picker_end,
|
||||
HTML("<b>Style:</b>"),
|
||||
dropdown_styles,
|
||||
HTML("<b>Colour percentile stretch:</b>"),
|
||||
slider_percentile,
|
||||
HTML("<b>Maximum cloud cover (%):</b>"),
|
||||
floatslider_max_cloud_cover,
|
||||
checkbox_rolling_median,
|
||||
text_rolling_median_window,
|
||||
HTML("</br><b>Output file format:</b>"),
|
||||
dropdown_output,
|
||||
HTML("</br>"),
|
||||
expand,
|
||||
]
|
||||
)
|
||||
map_selection = VBox(
|
||||
[
|
||||
HTML("</br><b>Map overlay:</b>"),
|
||||
dropdown_basemap,
|
||||
]
|
||||
)
|
||||
parameter_selection.layout = make_box_layout()
|
||||
map_selection.layout = make_box_layout()
|
||||
|
||||
###############################
|
||||
# SPECIFICATION OF APP LAYOUT #
|
||||
###############################
|
||||
|
||||
# 0 1 2 3 4 5 6 7 8 9
|
||||
# ---------------------------------------------
|
||||
# 0 | Header | Map sel. |
|
||||
# |-------------------------------------------|
|
||||
# 1 | Params | |
|
||||
# 2 | | |
|
||||
# 3 | | |
|
||||
# 4 | | Map |
|
||||
# 5 | | |
|
||||
# |--------| |
|
||||
# 6 | Run | |
|
||||
# |-------------------------------------------|
|
||||
# 7 | Status info | Figure/output |
|
||||
# 8 | | |
|
||||
# 9 | | |
|
||||
# 10 | | |
|
||||
# 11 ---------------------------------------------
|
||||
|
||||
# Create the layout #[rowspan, colspan]
|
||||
grid = GridspecLayout(12, 10, height="1500px", width="auto")
|
||||
|
||||
# Header and controls
|
||||
grid[0, :8] = self.header
|
||||
grid[0, 8:] = map_selection
|
||||
grid[1:6, 0:2] = parameter_selection
|
||||
grid[6, 0:2] = run_button
|
||||
|
||||
# Status info, map and plot
|
||||
grid[1:7, 2:] = self.m # map
|
||||
grid[7:, 0:4] = self.status_info
|
||||
grid[7:, 4:] = self.output_plot
|
||||
|
||||
# Display using HBox children attribute
|
||||
self.children = [grid]
|
||||
|
||||
######################################
|
||||
# DEFINITION OF ALL UPDATE FUNCTIONS #
|
||||
######################################
|
||||
|
||||
# Update date
|
||||
def update_start_date(self, change):
|
||||
self.start_date = str(change.new)
|
||||
|
||||
# Clear data load params to trigger data re-load
|
||||
self.timeseries_ds = None
|
||||
self.load_params = None
|
||||
self.query_params = None
|
||||
|
||||
# Update date
|
||||
def update_end_date(self, change):
|
||||
self.end_date = str(change.new)
|
||||
|
||||
# Clear data load params to trigger data re-load
|
||||
self.timeseries_ds = None
|
||||
self.load_params = None
|
||||
self.query_params = None
|
||||
|
||||
# Update colour stretch
|
||||
def update_slider_percentile(self, change):
|
||||
self.vmin, self.vmax = change.new
|
||||
|
||||
# Update power transform
|
||||
def update_slider_power(self, change):
|
||||
self.power = change.new
|
||||
|
||||
# Update good data slider
|
||||
def update_floatslider_max_cloud_cover(self, change):
|
||||
self.max_cloud_cover = change.new
|
||||
|
||||
# Clear data load params to trigger data re-load
|
||||
self.timeseries_ds = None
|
||||
self.load_params = None
|
||||
self.query_params = None
|
||||
|
||||
# Enable unsharp masking and show/hide custom params
|
||||
def update_checkbox_unsharp_mask(self, change):
|
||||
self.unsharp_mask = change.new
|
||||
|
||||
# Show unsharp masking params in menu if activated
|
||||
if change.new:
|
||||
self.text_unsharp_mask_radius.layout.display = "block"
|
||||
self.text_unsharp_mask_amount.layout.display = "block"
|
||||
else:
|
||||
self.text_unsharp_mask_radius.layout.display = "none"
|
||||
self.text_unsharp_mask_amount.layout.display = "none"
|
||||
|
||||
# Change unsharp masking radius
|
||||
def update_text_unsharp_mask_radius(self, change):
|
||||
self.unsharp_mask_radius = change.new
|
||||
|
||||
# Change unsharp masking amount
|
||||
def update_text_unsharp_mask_amount(self, change):
|
||||
self.unsharp_mask_amount = change.new
|
||||
|
||||
# Enable rolling median and show/hide custom params
|
||||
def update_checkbox_rolling_median(self, change):
|
||||
self.rolling_median = change.new
|
||||
|
||||
# Show rolling median params in menu if activated
|
||||
if change.new:
|
||||
self.text_rolling_median_window.layout.display = "block"
|
||||
else:
|
||||
self.text_rolling_median_window.layout.display = "none"
|
||||
|
||||
# Change rolling median window
|
||||
def update_text_rolling_median_window(self, change):
|
||||
self.rolling_median_window = change.new
|
||||
|
||||
# Override max size limit
|
||||
def update_checkbox_max_size(self, change):
|
||||
self.max_size = change.new
|
||||
|
||||
# Add DE Africa Coastlines overlay
|
||||
def update_deacoastlines(self, change):
|
||||
self.deacoastlines = change.new
|
||||
|
||||
# Apply cloud mask in load_ard
|
||||
def update_checkbox_cloud_mask(self, change):
|
||||
self.cloud_mask = change.new
|
||||
|
||||
# Clear data load params to trigger data re-load
|
||||
self.timeseries_ds = None
|
||||
self.load_params = None
|
||||
self.query_params = None
|
||||
|
||||
# Override min width
|
||||
def update_width(self, change):
|
||||
self.width = change.new
|
||||
|
||||
# Override interval
|
||||
def update_interval(self, change):
|
||||
self.interval = change.new
|
||||
|
||||
# Update resolution
|
||||
def update_text_resolution(self, change):
|
||||
self.resolution = change.new
|
||||
|
||||
# Clear data load params to trigger data re-load
|
||||
self.timeseries_ds = None
|
||||
self.load_params = None
|
||||
self.query_params = None
|
||||
|
||||
# Change layers shown on the map
|
||||
def update_dealayer(self, change):
|
||||
self.dealayer = change.new
|
||||
|
||||
if change.new == "Landsat":
|
||||
self.text_resolution.value = 30
|
||||
|
||||
else:
|
||||
self.text_resolution.value = 10
|
||||
|
||||
# Update basemap
|
||||
def update_basemap(self, change):
|
||||
self.basemap = change.new
|
||||
update_map_layers(self)
|
||||
|
||||
# Set imagery style
|
||||
def update_styles(self, change):
|
||||
self.style = change.new
|
||||
|
||||
# Clear data load params to trigger data re-load
|
||||
self.timeseries_ds = None
|
||||
self.load_params = None
|
||||
self.query_params = None
|
||||
|
||||
# Set output file format
|
||||
def update_output(self, change):
|
||||
self.output_format = change.new
|
||||
|
||||
# Set output file format
|
||||
def update_dropdown_resampling(self, change):
|
||||
self.resample_freq = change.new
|
||||
|
||||
def run_app(self, change):
|
||||
|
||||
# Clear progress bar and output areas before running
|
||||
self.status_info.clear_output()
|
||||
self.output_plot.clear_output()
|
||||
|
||||
# Verify that polygon was drawn
|
||||
if self.gdf_drawn is not None:
|
||||
|
||||
with self.status_info:
|
||||
|
||||
# Load data and add to attribute
|
||||
if self.timeseries_ds is None:
|
||||
self.timeseries_ds = extract_data(self)
|
||||
|
||||
else:
|
||||
print("Using previously loaded data")
|
||||
|
||||
if self.timeseries_ds is not None:
|
||||
|
||||
with self.status_info:
|
||||
|
||||
# Create unique file name
|
||||
centre_coords = self.gdf_drawn.geometry[0].centroid.coords[0][::-1]
|
||||
site = reverse_geocode(coords=centre_coords)
|
||||
fname = (
|
||||
f"{self.dealayer}_{site}_{self.start_date}_"
|
||||
f"{self.end_date}_{self.style}_{self.resolution:.0f}m."
|
||||
f"{self.output_format}".replace(" ", "")
|
||||
.replace(",", "")
|
||||
.lower()
|
||||
)
|
||||
|
||||
print(
|
||||
f"\nExporting animation for {site}.\nThis may take several minutes..."
|
||||
)
|
||||
|
||||
############
|
||||
# Plotting #
|
||||
############
|
||||
|
||||
with self.output_plot:
|
||||
plot_data(self, fname)
|
||||
|
||||
else:
|
||||
with self.status_info:
|
||||
print(
|
||||
"No satellite data found in the selected area. "
|
||||
"Please select a new rectangle over an area with "
|
||||
"satellite imagery."
|
||||
)
|
||||
|
||||
else:
|
||||
with self.status_info:
|
||||
print(
|
||||
'Please draw a valid rectangle on the map, then press "Generate animation".'
|
||||
)
|
||||
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Loading and interacting with data in the change filmstrips notebook,
|
||||
inside the Real_world_examples folder.
|
||||
"""
|
||||
|
||||
# Load modules
|
||||
import os
|
||||
import dask
|
||||
import datacube
|
||||
import warnings
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import xarray as xr
|
||||
import matplotlib.pyplot as plt
|
||||
from odc.algo import geomedian_with_mads
|
||||
from odc.ui import select_on_a_map
|
||||
from dask.utils import parse_bytes
|
||||
from datacube.utils.geometry import CRS, assign_crs
|
||||
from datacube.utils.rio import configure_s3_access
|
||||
from datacube.utils.dask import start_local_dask
|
||||
from ipyleaflet import basemaps, basemap_to_tiles
|
||||
|
||||
# Load utility functions
|
||||
from deafrica_tools.datahandling import load_ard, mostcommon_crs
|
||||
from deafrica_tools.dask import create_local_dask_cluster
|
||||
|
||||
|
||||
def run_filmstrip_app(
|
||||
output_name,
|
||||
time_range,
|
||||
time_step,
|
||||
tide_range=(0.0, 1.0),
|
||||
resolution=(-30, 30),
|
||||
max_cloud=0.5,
|
||||
ls7_slc_off=False,
|
||||
size_limit=10000,
|
||||
):
|
||||
"""
|
||||
An interactive app that allows the user to select a region from a
|
||||
map, then load Digital Earth Africa Landsat data and combine it
|
||||
using the geometric median ("geomedian") statistic to reveal the
|
||||
median or 'typical' appearance of the landscape for a series of
|
||||
time periods.
|
||||
|
||||
The results for each time period are combined into a 'filmstrip'
|
||||
plot which visualises how the landscape has changed in appearance
|
||||
across time, with a 'change heatmap' panel highlighting potential
|
||||
areas of greatest change.
|
||||
|
||||
For coastal applications, the analysis can be customised to select
|
||||
only satellite images obtained during a specific tidal range
|
||||
(e.g. low, average or high tide).
|
||||
|
||||
Last modified: April 2020
|
||||
|
||||
Parameters
|
||||
----------
|
||||
output_name : str
|
||||
A name that will be used to name the output filmstrip plot file.
|
||||
time_range : tuple
|
||||
A tuple giving the date range to analyse
|
||||
(e.g. `time_range = ('1988-01-01', '2017-12-31')`).
|
||||
time_step : dict
|
||||
This parameter sets the length of the time periods to compare
|
||||
(e.g. `time_step = {'years': 5}` will generate one filmstrip
|
||||
plot for every five years of data; `time_step = {'months': 18}`
|
||||
will generate one plot for each 18 month period etc. Time
|
||||
periods are counted from the first value given in `time_range`.
|
||||
tide_range : tuple, optional
|
||||
An optional parameter that can be used to generate filmstrip
|
||||
plots based on specific ocean tide conditions. This can be
|
||||
valuable for analysing change consistently along the coast.
|
||||
For example, `tide_range = (0.0, 0.2)` will select only
|
||||
satellite images acquired at the lowest 20% of tides;
|
||||
`tide_range = (0.8, 1.0)` will select images from the highest
|
||||
20% of tides. The default is `tide_range = (0.0, 1.0)` which
|
||||
will select all images regardless of tide.
|
||||
resolution : tuple, optional
|
||||
The spatial resolution to load data. The default is
|
||||
`resolution = (-30, 30)`, which will load data at 30 m pixel
|
||||
resolution. Increasing this (e.g. to `resolution = (-100, 100)`)
|
||||
can be useful for loading large spatial extents.
|
||||
max_cloud : float, optional
|
||||
This parameter can be used to exclude satellite images with
|
||||
excessive cloud. The default is `0.5`, which will keep all images
|
||||
with less than 50% cloud.
|
||||
ls7_slc_off : bool, optional
|
||||
An optional boolean indicating whether to include data from
|
||||
after the Landsat 7 SLC failure (i.e. SLC-off). Defaults to
|
||||
False, which removes all Landsat 7 observations > May 31 2003.
|
||||
size_limit : int, optional
|
||||
An optional integer (in hectares) specifying the size limit
|
||||
for the data query. Queries larger than this size will receive
|
||||
a warning that he data query is too large (and may
|
||||
therefore result in memory errors).
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
ds_geomedian : xarray Dataset
|
||||
An xarray dataset containing geomedian composites for each
|
||||
timestep in the analysis.
|
||||
|
||||
"""
|
||||
|
||||
########################
|
||||
# Select and load data #
|
||||
########################
|
||||
|
||||
# Define centre_coords as a global variable
|
||||
global centre_coords
|
||||
|
||||
# Test if centre_coords is in the global namespace;
|
||||
# use default value if it isn't
|
||||
if "centre_coords" not in globals():
|
||||
centre_coords = (6.587292, 1.532833)
|
||||
|
||||
# Plot interactive map to select area
|
||||
basemap = basemap_to_tiles(basemaps.Esri.WorldImagery)
|
||||
geopolygon = select_on_a_map(height="600px",
|
||||
layers=(basemap,),
|
||||
center=centre_coords,
|
||||
zoom=14)
|
||||
|
||||
# Set centre coords based on most recent selection to re-focus
|
||||
# subsequent data selections
|
||||
centre_coords = geopolygon.centroid.points[0][::-1]
|
||||
|
||||
# Test size of selected area
|
||||
msq_per_hectare = 10000
|
||||
area = geopolygon.to_crs(crs=CRS("epsg:6933")).area / msq_per_hectare
|
||||
radius = np.round(np.sqrt(size_limit), 1)
|
||||
if area > size_limit:
|
||||
print(f"Warning: Your selected area is {area:.00f} hectares. "
|
||||
f"Please select an area of less than {size_limit} hectares."
|
||||
f"\nTo select a smaller area, re-run the cell "
|
||||
f"above and draw a new polygon.")
|
||||
|
||||
else:
|
||||
|
||||
print("Starting analysis...")
|
||||
|
||||
# Connect to datacube database
|
||||
dc = datacube.Datacube(app="Change_filmstrips")
|
||||
|
||||
# Configure local dask cluster
|
||||
client = create_local_dask_cluster(return_client=True)
|
||||
|
||||
# Obtain native CRS
|
||||
crs = mostcommon_crs(dc=dc,
|
||||
product="ls8_sr",
|
||||
query={
|
||||
"time": "2014",
|
||||
"geopolygon": geopolygon
|
||||
})
|
||||
|
||||
# Create query based on time range, area selected, custom params
|
||||
query = {
|
||||
"time": time_range,
|
||||
"geopolygon": geopolygon,
|
||||
"output_crs": crs,
|
||||
"resolution": resolution,
|
||||
"dask_chunks": {
|
||||
"x": 3000,
|
||||
"y": 3000
|
||||
},
|
||||
"align": (resolution[1] / 2.0, resolution[1] / 2.0),
|
||||
}
|
||||
|
||||
# Load data from all three Landsats
|
||||
warnings.filterwarnings("ignore")
|
||||
ds = load_ard(
|
||||
dc=dc,
|
||||
measurements=["red", "green", "blue"],
|
||||
products=["ls5_sr", "ls7_sr", "ls8_sr"],
|
||||
min_gooddata=max_cloud,
|
||||
ls7_slc_off=ls7_slc_off,
|
||||
**query,
|
||||
)
|
||||
|
||||
# Optionally calculate tides for each timestep in the satellite
|
||||
# dataset and drop any observations out side this range
|
||||
if tide_range != (0.0, 1.0):
|
||||
from deafrica_tools.coastal import tidal_tag
|
||||
ds = tidal_tag(ds=ds, tidepost_lat=None, tidepost_lon=None)
|
||||
min_tide, max_tide = ds.tide_height.quantile(tide_range).values
|
||||
ds = ds.sel(time=(ds.tide_height >= min_tide) &
|
||||
(ds.tide_height <= max_tide))
|
||||
ds = ds.drop("tide_height")
|
||||
print(f" Keeping {len(ds.time)} observations with tides "
|
||||
f"between {min_tide:.2f} and {max_tide:.2f} m")
|
||||
|
||||
# Create time step ranges to generate filmstrips from
|
||||
bins_dt = pd.date_range(start=time_range[0],
|
||||
end=time_range[1],
|
||||
freq=pd.DateOffset(**time_step))
|
||||
|
||||
# Bin all satellite observations by timestep. If some observations
|
||||
# fall outside the upper bin, label these with the highest bin
|
||||
labels = bins_dt.astype("str")
|
||||
time_steps = (pd.cut(ds.time.values, bins_dt,
|
||||
labels=labels[:-1]).add_categories(
|
||||
labels[-1]).fillna(labels[-1]))
|
||||
|
||||
time_steps_var = xr.DataArray(time_steps, [("time", ds.time.values)],
|
||||
name="timestep")
|
||||
|
||||
# Resample data temporally into time steps, and compute geomedians
|
||||
ds_geomedian = (ds.groupby(time_steps_var).apply(
|
||||
lambda ds_subset: geomedian_with_mads(
|
||||
ds_subset, compute_mads=False, compute_count=False)))
|
||||
|
||||
print("\nGenerating geomedian composites and plotting "
|
||||
"filmstrips... (click the Dashboard link above for status)")
|
||||
ds_geomedian = ds_geomedian.compute()
|
||||
|
||||
# Reset CRS that is lost during geomedian compositing
|
||||
ds_geomedian = assign_crs(ds_geomedian, crs=ds.geobox.crs)
|
||||
|
||||
############
|
||||
# Plotting #
|
||||
############
|
||||
|
||||
# Convert to array and extract vmin/vmax
|
||||
output_array = ds_geomedian[["red", "green", "blue"]].to_array()
|
||||
percentiles = output_array.quantile(q=(0.02, 0.98)).values
|
||||
|
||||
# Create the plot with one subplot more than timesteps in the
|
||||
# dataset. Figure width is set based on the number of subplots
|
||||
# and aspect ratio
|
||||
n_obs = output_array.sizes["timestep"]
|
||||
ratio = output_array.sizes["x"] / output_array.sizes["y"]
|
||||
fig, axes = plt.subplots(1,
|
||||
n_obs + 1,
|
||||
figsize=(5 * ratio * (n_obs + 1), 5))
|
||||
fig.subplots_adjust(wspace=0.05, hspace=0.05)
|
||||
|
||||
# Add timesteps to the plot, set aspect to equal to preserve shape
|
||||
for i, ax_i in enumerate(axes.flatten()[:n_obs]):
|
||||
output_array.isel(timestep=i).plot.imshow(ax=ax_i,
|
||||
vmin=percentiles[0],
|
||||
vmax=percentiles[1])
|
||||
ax_i.get_xaxis().set_visible(False)
|
||||
ax_i.get_yaxis().set_visible(False)
|
||||
ax_i.set_aspect("equal")
|
||||
|
||||
# Add change heatmap panel to final subplot. Heatmap is computed
|
||||
# by first taking the log of the array (so change in dark areas
|
||||
# can be identified), then computing standard deviation between
|
||||
# all timesteps
|
||||
(np.log(output_array).std(dim=["timestep"]).mean(
|
||||
dim="variable").plot.imshow(ax=axes.flatten()[-1],
|
||||
robust=True,
|
||||
cmap="magma",
|
||||
add_colorbar=False))
|
||||
axes.flatten()[-1].get_xaxis().set_visible(False)
|
||||
axes.flatten()[-1].get_yaxis().set_visible(False)
|
||||
axes.flatten()[-1].set_aspect("equal")
|
||||
axes.flatten()[-1].set_title("Change heatmap")
|
||||
|
||||
# Export to file
|
||||
date_string = "_".join(time_range)
|
||||
ts_v = list(time_step.values())[0]
|
||||
ts_k = list(time_step.keys())[0]
|
||||
fig.savefig(
|
||||
f"filmstrip_{output_name}_{date_string}_{ts_v}{ts_k}.png",
|
||||
dpi=150,
|
||||
bbox_inches="tight",
|
||||
pad_inches=0.1,
|
||||
)
|
||||
|
||||
# close dask client
|
||||
client.shutdown()
|
||||
|
||||
return ds_geomedian
|
||||
@@ -0,0 +1,337 @@
|
||||
# crophealth.py
|
||||
'''
|
||||
Functions for loading and interacting with data in the crop health notebook,
|
||||
inside the Real_world_examples folder.
|
||||
'''
|
||||
|
||||
# Load modules
|
||||
|
||||
# Force GeoPandas to use Shapely instead of PyGEOS
|
||||
# In a future release, GeoPandas will switch to using Shapely by default.
|
||||
import os
|
||||
os.environ['USE_PYGEOS'] = '0'
|
||||
|
||||
from ipyleaflet import (
|
||||
Map,
|
||||
GeoJSON,
|
||||
DrawControl,
|
||||
basemaps
|
||||
)
|
||||
import datetime as dt
|
||||
import datacube
|
||||
from osgeo import ogr
|
||||
import matplotlib as mpl
|
||||
import matplotlib.pyplot as plt
|
||||
import rasterio
|
||||
from rasterio.features import geometry_mask
|
||||
import xarray as xr
|
||||
from IPython.display import display
|
||||
import warnings
|
||||
import ipywidgets as widgets
|
||||
import json
|
||||
import geopandas as gpd
|
||||
from io import BytesIO
|
||||
|
||||
# Load utility functions
|
||||
from deafrica_tools.datahandling import load_ard
|
||||
from deafrica_tools.spatial import xr_rasterize
|
||||
from deafrica_tools.bandindices import calculate_indices
|
||||
|
||||
|
||||
def load_crophealth_data(lat, lon, buffer, date):
|
||||
"""
|
||||
Loads Sentinel-2 analysis-ready data (ARD) product for the crop health
|
||||
case-study area over the last two years.
|
||||
Last modified: April 2020
|
||||
|
||||
Parameters
|
||||
----------
|
||||
lat: float
|
||||
The central latitude to analyse
|
||||
lon: float
|
||||
The central longitude to analyse
|
||||
buffer:
|
||||
The number of square degrees to load around the central latitude and longitude.
|
||||
For reasonable loading times, set this as `0.1` or lower.
|
||||
date:
|
||||
The most recent date to show data for.
|
||||
The app will automatically load all data available for the two years prior to this date.
|
||||
|
||||
Returns
|
||||
----------
|
||||
ds: xarray.Dataset
|
||||
data set containing combined, masked data
|
||||
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='Crophealth-app')
|
||||
|
||||
# Define area to load
|
||||
latitude = (lat - buffer, lat + buffer)
|
||||
longitude = (lon - buffer, lon + buffer)
|
||||
|
||||
# Specify the date range
|
||||
# Calculated as today's date, subtract 730 days to collect two years of data
|
||||
# Dates are converted to strings as required by loading function below
|
||||
end_date = dt.datetime.strptime(date, "%Y-%m-%d")
|
||||
start_date = end_date - dt.timedelta(days=730)
|
||||
|
||||
time = (start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"))
|
||||
|
||||
# Construct the data cube query
|
||||
products = ["s2_l2a"]
|
||||
|
||||
query = {
|
||||
'x': longitude,
|
||||
'y': latitude,
|
||||
'time': time,
|
||||
'measurements': [
|
||||
'red',
|
||||
'green',
|
||||
'blue',
|
||||
'nir',
|
||||
'swir_2'
|
||||
],
|
||||
'output_crs': 'EPSG:6933',
|
||||
'resolution': (-20, 20)
|
||||
}
|
||||
|
||||
# Load the data and mask out bad quality pixels
|
||||
ds = load_ard(dc, products=products, min_gooddata=0.5, **query)
|
||||
|
||||
# Calculate the normalised difference vegetation index (NDVI) across
|
||||
# all pixels for each image.
|
||||
# This is stored as an attribute of the data
|
||||
ds = calculate_indices(ds, index='NDVI', satellite_mission='s2')
|
||||
|
||||
# Return the data
|
||||
return(ds)
|
||||
|
||||
|
||||
def run_crophealth_app(ds, lat, lon, buffer):
|
||||
"""
|
||||
Plots an interactive map of the crop health case-study area and allows
|
||||
the user to draw polygons. This returns a plot of the average NDVI value
|
||||
in the polygon area.
|
||||
Last modified: January 2020
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ds: xarray.Dataset
|
||||
data set containing combined, masked data
|
||||
Masked values are set to 'nan'
|
||||
lat: float
|
||||
The central latitude corresponding to the area of loaded ds
|
||||
lon: float
|
||||
The central longitude corresponding to the area of loaded ds
|
||||
buffer:
|
||||
The number of square degrees to load around the central latitude and longitude.
|
||||
For reasonable loading times, set this as `0.1` or lower.
|
||||
"""
|
||||
|
||||
# Suppress warnings
|
||||
warnings.filterwarnings('ignore')
|
||||
|
||||
# Update plotting functionality through rcParams
|
||||
mpl.rcParams.update({'figure.autolayout': True})
|
||||
|
||||
# Define polygon bounds
|
||||
latitude = (lat - buffer, lat + buffer)
|
||||
longitude = (lon - buffer, lon + buffer)
|
||||
|
||||
# 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": [
|
||||
[
|
||||
[
|
||||
longitude[0],
|
||||
latitude[0]
|
||||
],
|
||||
[
|
||||
longitude[1],
|
||||
latitude[0]
|
||||
],
|
||||
[
|
||||
longitude[1],
|
||||
latitude[1]
|
||||
],
|
||||
[
|
||||
longitude[0],
|
||||
latitude[1]
|
||||
],
|
||||
[
|
||||
longitude[0],
|
||||
latitude[0]
|
||||
]
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
# 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
|
||||
loadeddata_geometry = ogr.CreateGeometryFromJson(str(geom_obj['geometry']))
|
||||
loadeddata_center = [
|
||||
loadeddata_geometry.Centroid().GetY(),
|
||||
loadeddata_geometry.Centroid().GetX()
|
||||
]
|
||||
loadeddata_zoom = 16
|
||||
|
||||
# define the study area map
|
||||
studyarea_map = Map(
|
||||
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 a plot of "
|
||||
"average NDVI over time in that area.")
|
||||
|
||||
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(figsize=(8, 6))
|
||||
ax.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
|
||||
|
||||
# Execute behaviour based on what the user draws
|
||||
if geo_json['geometry']['type'] == 'Polygon':
|
||||
|
||||
info.clear_output(wait=True) # wait=True reduces flicker effect
|
||||
|
||||
# Save geojson polygon to io temporary file to be rasterized later
|
||||
jsonData = json.dumps(geo_json)
|
||||
binaryData = jsonData.encode()
|
||||
io = BytesIO(binaryData)
|
||||
io.seek(0)
|
||||
|
||||
# Read the polygon as a geopandas dataframe
|
||||
gdf = gpd.read_file(io)
|
||||
gdf.crs = "EPSG:4326"
|
||||
|
||||
# Convert the drawn geometry to pixel coordinates
|
||||
xr_poly = xr_rasterize(gdf, ds.NDVI.isel(time=0), crs='EPSG:6933')
|
||||
|
||||
# Construct a mask to only select pixels within the drawn polygon
|
||||
masked_ds = ds.NDVI.where(xr_poly)
|
||||
|
||||
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 new data to the plot
|
||||
xr.plot.plot(
|
||||
masked_ds_mean,
|
||||
marker='*',
|
||||
color=colour,
|
||||
ax=ax
|
||||
)
|
||||
|
||||
# reset titles back to custom
|
||||
ax.set_title("Average NDVI from Sentinel-2")
|
||||
ax.set_xlabel("Date")
|
||||
ax.set_ylabel("NDVI")
|
||||
|
||||
# refresh display
|
||||
fig_display.clear_output(wait=True) # wait=True reduces flicker effect
|
||||
with fig_display:
|
||||
display(fig)
|
||||
|
||||
with info:
|
||||
print("Plot status: polygon sucessfully 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)
|
||||
@@ -0,0 +1,505 @@
|
||||
"""
|
||||
Digital Earth Africa Coastline widget, which can be used to
|
||||
interactively extract shoreline data using transects.
|
||||
"""
|
||||
|
||||
# Import required packages
|
||||
|
||||
# Force GeoPandas to use Shapely instead of PyGEOS
|
||||
# In a future release, GeoPandas will switch to using Shapely by default.
|
||||
import os
|
||||
os.environ['USE_PYGEOS'] = '0'
|
||||
|
||||
import fiona
|
||||
import sys
|
||||
import datacube
|
||||
import warnings
|
||||
import matplotlib.pyplot as plt
|
||||
from datacube.utils.geometry import CRS
|
||||
from ipyleaflet import (
|
||||
WMSLayer,
|
||||
basemaps,
|
||||
basemap_to_tiles,
|
||||
Map,
|
||||
DrawControl,
|
||||
WidgetControl,
|
||||
LayerGroup,
|
||||
LayersControl,
|
||||
GeoData,
|
||||
)
|
||||
from traitlets import Unicode
|
||||
from ipywidgets import (
|
||||
GridspecLayout,
|
||||
Button,
|
||||
Layout,
|
||||
HBox,
|
||||
VBox,
|
||||
HTML,
|
||||
Output,
|
||||
)
|
||||
import json
|
||||
import geopandas as gpd
|
||||
from io import BytesIO
|
||||
import ipywidgets as widgets
|
||||
|
||||
import deafrica_tools.app.widgetconstructors as deawidgets
|
||||
from deafrica_tools.coastal import get_coastlines, transect_distances
|
||||
from owslib.wms import WebMapService
|
||||
|
||||
def make_box_layout():
|
||||
return Layout(
|
||||
# border='solid 1px black',
|
||||
margin='0px 10px 10px 0px',
|
||||
padding='5px 5px 5px 5px',
|
||||
width='100%',
|
||||
height='100%',
|
||||
)
|
||||
|
||||
|
||||
def create_expanded_button(description, button_style):
|
||||
return Button(
|
||||
description=description,
|
||||
button_style=button_style,
|
||||
layout=Layout(width="auto", height="auto"),
|
||||
)
|
||||
|
||||
|
||||
class transect_app(HBox):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
######################
|
||||
# INITIAL ATTRIBUTES #
|
||||
######################
|
||||
|
||||
self.output_name = "example_output"
|
||||
self.export_csv = False
|
||||
self.export_plot = False
|
||||
self.product_list = [
|
||||
("ESRI World Imagery", "none"),
|
||||
("Open Street Map", "open_street_map"),
|
||||
]
|
||||
self.product = self.product_list[0][1]
|
||||
self.mode_list = [('Distance', 'distance'), ('Width', 'width')]
|
||||
self.mode = self.mode_list[0][1]
|
||||
self.target = None
|
||||
self.action = None
|
||||
self.gdf_drawn = None
|
||||
self.gdf_uploaded = None
|
||||
|
||||
##################
|
||||
# HEADER FOR APP #
|
||||
##################
|
||||
|
||||
# Create the Header widget
|
||||
header_title_text = "<h3>Digital Earth Africa Coastlines shoreline transect extraction</h3>"
|
||||
instruction_text = "Select parameters and draw a transect on the map to extract shoreline data. <b>In distance mode</b>, draw a transect line starting from land that crosses multiple shorelines. <br><b>In width mode</b>, draw a transect line that intersects shorelines at least twice. Alternatively, <b>upload an vector file</b> to extract shoreline data for multiple existing transects."
|
||||
self.header = deawidgets.create_html(
|
||||
f"{header_title_text}<p>{instruction_text}</p>")
|
||||
self.header.layout = make_box_layout()
|
||||
|
||||
#####################################
|
||||
# HANDLER FUNCTION FOR DRAW CONTROL #
|
||||
#####################################
|
||||
|
||||
# Define the action to take once something is drawn on the map
|
||||
def update_geojson(target, action, geo_json):
|
||||
|
||||
# Remove previously uploaded data if present
|
||||
self.gdf_uploaded = None
|
||||
fileupload_transects._counter = 0
|
||||
|
||||
# Get data from action
|
||||
self.action = action
|
||||
|
||||
# Convert data to geopandas
|
||||
json_data = json.dumps(geo_json)
|
||||
binary_data = json_data.encode()
|
||||
io = BytesIO(binary_data)
|
||||
io.seek(0)
|
||||
gdf = gpd.read_file(io)
|
||||
gdf.crs = "EPSG:4326"
|
||||
|
||||
# Convert to WGS 84 / NSIDC EASE-Grid 2.0 Global and compute area
|
||||
gdf_drawn_nsidc = gdf.copy().to_crs("EPSG:6933")
|
||||
m2_per_km2 = 10**6
|
||||
area = gdf_drawn_nsidc.envelope.area.values[0] / m2_per_km2
|
||||
polyarea_label = 'Total area of DE Africa Coastlines data to extract'
|
||||
polyarea_text = f"<b>{polyarea_label}</b>: {area:.2f} km<sup>2</sup>"
|
||||
|
||||
# Test area size
|
||||
if area <= 50000:
|
||||
confirmation_text = '<span style="color: #33cc33"> <b>(Area to extract falls within recommended limit; click "Extract shoreline data" to continue)</b></span>'
|
||||
self.header.value = header_title_text + polyarea_text + confirmation_text
|
||||
self.gdf_drawn = gdf
|
||||
else:
|
||||
warning_text = '<span style="color: #ff5050"> <b>(Area to extract is too large, please select a smaller transect)</b></span>'
|
||||
self.header.value = header_title_text + polyarea_text + warning_text
|
||||
self.gdf_drawn = None
|
||||
|
||||
###########################
|
||||
# WIDGETS FOR APP OUTPUTS #
|
||||
###########################
|
||||
|
||||
self.status_info = Output(layout=make_box_layout())
|
||||
self.output_plot = Output(layout=make_box_layout())
|
||||
|
||||
#########################################
|
||||
# MAP WIDGET, DRAWING TOOLS, WMS LAYERS #
|
||||
#########################################
|
||||
|
||||
# Create drawing tools
|
||||
desired_drawtools = ['polyline']
|
||||
draw_control = deawidgets.create_drawcontrol(desired_drawtools)
|
||||
|
||||
# Load DEACoastLines WMS
|
||||
deacl_url = "https://geoserver.digitalearth.africa/geoserver/wms"
|
||||
deacl_layer = "coastlines:DEAfrica_Coastlines"
|
||||
deacoastlines = WMSLayer(
|
||||
url=deacl_url,
|
||||
layers=deacl_layer,
|
||||
format='image/png',
|
||||
transparent=True,
|
||||
attribution='DE Africa Coastlines © 2022 Digital Earth Africa')
|
||||
|
||||
# Begin by displaying an empty layer group, and update the group with desired WMS on interaction.
|
||||
self.map_layers = LayerGroup(layers=(deacoastlines,))
|
||||
self.map_layers.name = 'Map Overlays'
|
||||
|
||||
# Create map widget
|
||||
self.m = deawidgets.create_map(map_center=(0.5273, 25.1367),
|
||||
zoom_level=3,
|
||||
basemap=basemaps.Esri.WorldImagery)
|
||||
self.m.layout = make_box_layout()
|
||||
|
||||
# Add tools to map widget
|
||||
self.m.add_control(draw_control)
|
||||
self.m.add_layer(self.map_layers)
|
||||
|
||||
# Store current basemap for future use
|
||||
self.basemap = self.m.basemap
|
||||
|
||||
############################
|
||||
# WIDGETS FOR APP CONTROLS #
|
||||
############################
|
||||
|
||||
# Create parameter widgets
|
||||
text_output_name = deawidgets.create_inputtext(self.output_name,
|
||||
self.output_name)
|
||||
checkbox_csv = deawidgets.create_checkbox(self.export_csv,
|
||||
'Distance table (.csv)')
|
||||
checkbox_plot = deawidgets.create_checkbox(self.export_plot,
|
||||
'Figure (.png)')
|
||||
deaoverlay_dropdown = deawidgets.create_dropdown(
|
||||
self.product_list, self.product_list[0][1])
|
||||
mode_dropdown = deawidgets.create_dropdown(self.mode_list,
|
||||
self.mode_list[0][1])
|
||||
run_button = create_expanded_button("Extract shoreline data", "info")
|
||||
fileupload_transects = widgets.FileUpload(accept='', multiple=True)
|
||||
|
||||
####################################
|
||||
# UPDATE FUNCTIONS FOR EACH WIDGET #
|
||||
####################################
|
||||
|
||||
# Run update functions whenever various widgets are changed.
|
||||
text_output_name.observe(self.update_text_output_name, "value")
|
||||
checkbox_csv.observe(self.update_checkbox_csv, "value")
|
||||
checkbox_plot.observe(self.update_checkbox_plot, "value")
|
||||
deaoverlay_dropdown.observe(self.update_deaoverlay, "value")
|
||||
mode_dropdown.observe(self.update_mode, "value")
|
||||
run_button.on_click(self.run_app)
|
||||
draw_control.on_draw(update_geojson)
|
||||
fileupload_transects.observe(self.update_fileupload_transects, "value")
|
||||
|
||||
##################################
|
||||
# COLLECTION OF ALL APP CONTROLS #
|
||||
##################################
|
||||
|
||||
parameter_selection = VBox([
|
||||
HTML("<b>Output name:</b>"), text_output_name,
|
||||
HTML(
|
||||
'<b>Transect extraction mode:</b><br><img src="https://i.imgur.com/9fdTH9C.png">'
|
||||
),
|
||||
mode_dropdown,
|
||||
HTML("<b></br>Output files:</b>"),
|
||||
checkbox_plot,
|
||||
checkbox_csv,
|
||||
HTML(
|
||||
"</br><i><b>Advanced</b></br>Upload a GeoJSON or ESRI "
|
||||
"Shapefile (<5 mb) containing one or more transect lines.</i>"),
|
||||
fileupload_transects
|
||||
])
|
||||
map_selection = VBox([
|
||||
HTML("</br><b>Map overlay:</b>"),
|
||||
deaoverlay_dropdown,
|
||||
])
|
||||
parameter_selection.layout = make_box_layout()
|
||||
map_selection.layout = make_box_layout()
|
||||
|
||||
###############################
|
||||
# SPECIFICATION OF APP LAYOUT #
|
||||
###############################
|
||||
|
||||
# 0 1 2 3 4 5 6 7 8 9
|
||||
# ---------------------------------------------
|
||||
# 0 | Header | Map sel. |
|
||||
# ---------------------------------------------
|
||||
# 1 | Params | |
|
||||
# 2 | | |
|
||||
# 3 | | |
|
||||
# 4 | | Map |
|
||||
# 5 | | |
|
||||
# ---------- |
|
||||
# 6 | Run | |
|
||||
# ---------------------------------------------
|
||||
# 7 | Status info |
|
||||
# ---------------------------------------------
|
||||
# 8 | |
|
||||
# 9 | Output/figure |
|
||||
# 10 | |
|
||||
# 11 | ------------------------------------------|
|
||||
|
||||
# Create the layout #[rowspan, colspan]
|
||||
grid = GridspecLayout(12, 10, height="1350px", width="auto")
|
||||
|
||||
# Header and controls
|
||||
grid[0, :8] = self.header
|
||||
grid[0, 8:] = map_selection
|
||||
grid[1:6, 0:2] = parameter_selection
|
||||
grid[6, 0:2] = run_button
|
||||
|
||||
# Status info, map and plot
|
||||
grid[1:7, 2:] = self.m # map
|
||||
grid[7:8, :] = self.status_info
|
||||
grid[8:, :] = self.output_plot
|
||||
|
||||
# Display using HBox children attribute
|
||||
self.children = [grid]
|
||||
|
||||
######################################
|
||||
# DEFINITION OF ALL UPDATE FUNCTIONS #
|
||||
######################################
|
||||
|
||||
# Set the output csv
|
||||
def update_fileupload_transects(self, change):
|
||||
|
||||
# Clear any drawn data if present
|
||||
self.gdf_drawn = None
|
||||
|
||||
# Save to file
|
||||
for uploaded_filename in change.new.keys():
|
||||
with open(uploaded_filename, "wb") as output_file:
|
||||
content = change.new[uploaded_filename]['content']
|
||||
output_file.write(content)
|
||||
|
||||
with self.status_info:
|
||||
|
||||
try:
|
||||
|
||||
print('Loading vector data...', end='\r')
|
||||
valid_files = [
|
||||
file for file in change.new.keys()
|
||||
if file.lower().endswith(('.shp', '.geojson'))
|
||||
]
|
||||
valid_file = valid_files[0]
|
||||
transect_gdf = (gpd.read_file(valid_file).to_crs(
|
||||
"EPSG:4326").explode().reset_index(drop=True))
|
||||
|
||||
# Use ID column if it exists
|
||||
if 'id' in transect_gdf:
|
||||
transect_gdf = transect_gdf.set_index('id')
|
||||
print(f"Uploaded '{valid_file}'; automatically labelling "
|
||||
"transects using column 'id'.")
|
||||
else:
|
||||
print(
|
||||
f"Uploaded '{valid_file}'; no 'id' column detected, "
|
||||
f"labelling transects from 0 to {len(transect_gdf.index) - 1}."
|
||||
)
|
||||
|
||||
# Create a geodata
|
||||
geodata = GeoData(geo_dataframe=transect_gdf,
|
||||
style={
|
||||
'color': 'black',
|
||||
'weight': 3
|
||||
})
|
||||
|
||||
# Add to map
|
||||
xmin, ymin, xmax, ymax = transect_gdf.total_bounds
|
||||
self.m.fit_bounds([[ymin, xmin], [ymax, xmax]])
|
||||
self.m.add_layer(geodata)
|
||||
|
||||
# If completed, add to attribute
|
||||
self.gdf_uploaded = transect_gdf
|
||||
|
||||
except IndexError:
|
||||
print(
|
||||
"Cannot read uploaded files. Please ensure that data is "
|
||||
"in either GeoJSON or ESRI Shapefile format.",
|
||||
end='\r')
|
||||
self.gdf_uploaded = None
|
||||
|
||||
except fiona.errors.DriverError:
|
||||
print(
|
||||
"Shapefile is invalid. Please ensure that all shapefile "
|
||||
"components (e.g. .shp, .shx, .dbf, .prj) are uploaded.",
|
||||
end='\r')
|
||||
self.gdf_uploaded = None
|
||||
|
||||
# Set output name
|
||||
def update_text_output_name(self, change):
|
||||
self.output_name = change.new
|
||||
|
||||
# Output CSV
|
||||
def update_checkbox_csv(self, change):
|
||||
self.export_csv = change.new
|
||||
|
||||
# Output plot
|
||||
def update_checkbox_plot(self, change):
|
||||
self.export_plot = change.new
|
||||
|
||||
# Set mode
|
||||
def update_mode(self, change):
|
||||
self.mode = change.new
|
||||
|
||||
# Update product
|
||||
def update_deaoverlay(self, change):
|
||||
|
||||
self.product = change.new
|
||||
|
||||
# Load DE Africa CoastLines WMS
|
||||
deacl_url = "https://geoserver.digitalearth.africa/geoserver/wms"
|
||||
deacl_layer = "coastlines:DEAfrica_Coastlines"
|
||||
deacoastlines = WMSLayer(
|
||||
url=deacl_url,
|
||||
layers=deacl_layer,
|
||||
format="image/png",
|
||||
transparent=True,
|
||||
attribution="DE Africa Coastlines © 2022 Digital Earth Africa")
|
||||
|
||||
if self.product == "none":
|
||||
self.map_layers.clear_layers()
|
||||
self.map_layers.add_layer(deacoastlines)
|
||||
|
||||
elif self.product == "open_street_map":
|
||||
self.map_layers.clear_layers()
|
||||
layer = basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)
|
||||
self.map_layers.add_layer(layer)
|
||||
self.map_layers.add_layer(deacoastlines)
|
||||
|
||||
def run_app(self, change):
|
||||
|
||||
# Clear progress bar and output areas before running
|
||||
self.status_info.clear_output()
|
||||
self.output_plot.clear_output()
|
||||
|
||||
# Run DE Africa Coastlines analysis
|
||||
with self.status_info:
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
# Load transects from either map or uploaded files
|
||||
if self.gdf_uploaded is not None:
|
||||
transect_gdf = self.gdf_uploaded
|
||||
run_text = 'uploaded file'
|
||||
elif self.gdf_drawn is not None:
|
||||
transect_gdf = self.gdf_drawn
|
||||
transect_gdf.index = [self.output_name]
|
||||
run_text = 'selected transect'
|
||||
else:
|
||||
print(f'No transect drawn or uploaded. Please select a transect on the map, or upload a GeoJSON or ESRI Shapefile.',
|
||||
end='\r')
|
||||
transect_gdf = None
|
||||
|
||||
# If valid data was returned, load DEA Coastlines data
|
||||
if transect_gdf is not None:
|
||||
|
||||
# Load Coastlines data from WFS
|
||||
deacl_gdf = get_coastlines(bbox=transect_gdf)
|
||||
|
||||
# Test that data was correctly returned
|
||||
if len(deacl_gdf.index) > 0:
|
||||
|
||||
# Dissolve by year to remove duplicates, then sort by date
|
||||
deacl_gdf = deacl_gdf.dissolve(by='year', as_index=False)
|
||||
deacl_gdf['year'] = deacl_gdf.year.astype(int)
|
||||
deacl_gdf = deacl_gdf.sort_values('year')
|
||||
deacl_gdf = deacl_gdf.set_index('year')
|
||||
|
||||
else:
|
||||
print(
|
||||
"No annual shoreline data was found near the "
|
||||
"supplied transect. Please draw or select a new "
|
||||
"transect.",
|
||||
end='\r')
|
||||
deacl_gdf = None
|
||||
|
||||
# If valid DEA Coastlines data returned, calculate distances
|
||||
if deacl_gdf is not None:
|
||||
print(f'Analysing transect distances using "{self.mode}" mode...',
|
||||
end='\r')
|
||||
dist_df = transect_distances(
|
||||
transect_gdf.to_crs("EPSG:6933"),
|
||||
deacl_gdf.to_crs("EPSG:6933"),
|
||||
mode=self.mode)
|
||||
|
||||
# If valid data was produced:
|
||||
if dist_df.any(axis=None):
|
||||
|
||||
# Successful output
|
||||
print(f'DE Africa Coastlines data successfully extracted for {run_text}.')
|
||||
|
||||
# Export distance data
|
||||
if self.export_csv:
|
||||
|
||||
# Create folder if required and set path
|
||||
out_dir = 'deacoastlines_outputs'
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
csv_filename = f"{out_dir}/{self.output_name}.csv"
|
||||
|
||||
# Export to file
|
||||
dist_df.to_csv(csv_filename, index_label="Transect")
|
||||
print(f'Distance data exported to "{csv_filename}".')
|
||||
|
||||
# Generate plot
|
||||
with self.output_plot:
|
||||
|
||||
fig, ax = plt.subplots(constrained_layout=True,
|
||||
figsize=(15, 5.5))
|
||||
dist_df.T.plot(ax=ax, linewidth=3)
|
||||
|
||||
ax.legend(frameon=False, ncol=3, title='Transect')
|
||||
ax.set_title(f"Digital Earth Africa Coastlines transect extraction - {self.output_name}")
|
||||
ax.set_ylabel(f"Along-transect {self.mode} (m)")
|
||||
ax.set_xlim(dist_df.T.index[0], dist_df.T.index[-1])
|
||||
|
||||
# Hide the right and top spines
|
||||
ax.spines['right'].set_visible(False)
|
||||
ax.spines['top'].set_visible(False)
|
||||
|
||||
# Only show ticks on the left and bottom spines
|
||||
ax.yaxis.set_ticks_position('left')
|
||||
ax.xaxis.set_ticks_position('bottom')
|
||||
plt.show()
|
||||
|
||||
# Export plot
|
||||
with self.status_info:
|
||||
if self.export_plot:
|
||||
|
||||
# Create folder if required and set path
|
||||
out_dir = 'deacoastlines_outputs'
|
||||
os.makedirs(out_dir, exist_ok=True)
|
||||
figure_filename = f"{out_dir}/{self.output_name}.png"
|
||||
|
||||
# Export to file
|
||||
fig.savefig(figure_filename)
|
||||
print(f'Figure exported to "{figure_filename}".')
|
||||
|
||||
else:
|
||||
print(
|
||||
"No valid shoreline data intersects with the "
|
||||
"supplied transect. This can occur if:\n\n"
|
||||
" - the transect does not intersect with any shorelines\n"
|
||||
" - the transect intersects with shorelines more than once in 'distance' mode\n"
|
||||
" - the transect intersects with shorelines only once in 'width' mode\n\n"
|
||||
"Please draw or upload a new transect.",
|
||||
end='\r')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
"""
|
||||
Geomedian widget: generates an interactive visualisation of
|
||||
the geomedian summary statistic.
|
||||
"""
|
||||
|
||||
# Load modules
|
||||
import ipywidgets as widgets
|
||||
import matplotlib.pyplot as plt
|
||||
from mpl_toolkits.mplot3d import Axes3D
|
||||
import numpy as np
|
||||
import xarray as xr
|
||||
from odc.algo import xr_geomedian
|
||||
|
||||
def run_app():
|
||||
|
||||
"""
|
||||
An interactive app that allows users to visualise the difference between the median and geomedian time-series summary statistics. By modifying the red-green-blue values of three timesteps for a given pixel, the user changes the output summary statistics.
|
||||
|
||||
This allows a visual representation of the difference through the output values, RGB colour, as well as showing values plotted as a vector on a 3-dimensional space.
|
||||
|
||||
Last modified: December 2021
|
||||
"""
|
||||
|
||||
# Define the red-green-blue sliders for timestep 1
|
||||
p1r = widgets.IntSlider(description='Red', max=255, value=58)
|
||||
p1g = widgets.IntSlider(description='Green', max=255, value=153)
|
||||
p1b = widgets.IntSlider(description='Blue', max=255, value=68)
|
||||
|
||||
# Define the red-green-blue sliders for timestep 2
|
||||
p2r = widgets.IntSlider(description='Red', max=255, value=208)
|
||||
p2g = widgets.IntSlider(description='Green', max=255, value=221)
|
||||
p2b = widgets.IntSlider(description='Blue', max=255, value=203)
|
||||
|
||||
# Define the red-green-blue sliders for timestep 3
|
||||
p3r = widgets.IntSlider(description='Red', max=255, value=202)
|
||||
p3g = widgets.IntSlider(description='Green', max=255, value=82)
|
||||
p3b = widgets.IntSlider(description='Blue', max=255, value=33)
|
||||
|
||||
# Define the median calculation for the timesteps
|
||||
def f(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||
print('Red Median = {}'.format(np.median([p1r, p2r, p3r])))
|
||||
print('Green Median = {}'.format(np.median([p1g, p2g, p3g])))
|
||||
print('Blue Median = {}'.format(np.median([p1b, p2b, p3b])))
|
||||
|
||||
# Define the geomedian calculation for the timesteps
|
||||
def g(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||
print('Red Geomedian = {:.2f}'.format(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).red.values.ravel()[0]))
|
||||
print('Green Geomedian = {:.2f}'.format(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).green.values.ravel()[0]))
|
||||
print('Blue Geomedian = {:.2f}'.format(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).blue.values.ravel()[0]))
|
||||
|
||||
# Define the Timestep 1 box colour
|
||||
def h(p1r, p1g, p1b):
|
||||
fig1, axes1 = plt.subplots(figsize=(2,2))
|
||||
fig1 = plt.imshow([[(p1r, p1g, p1b)]])
|
||||
axes1.set_title('Timestep 1')
|
||||
axes1.axis('off')
|
||||
plt.show(fig1)
|
||||
|
||||
# Define the Timestep 2 box colour
|
||||
def hh(p2r, p2g, p2b):
|
||||
fig2, axes2 = plt.subplots(figsize=(2,2))
|
||||
fig2 = plt.imshow([[(p2r, p2g, p2b)]])
|
||||
axes2.set_title('Timestep 2')
|
||||
axes2.axis('off')
|
||||
plt.show(fig2)
|
||||
|
||||
# Define the Timestep 3 box colour
|
||||
def hhh(p3r, p3g, p3b):
|
||||
fig3, axes3 = plt.subplots(figsize=(2,2))
|
||||
fig3 = plt.imshow([[(p3r, p3g, p3b)]])
|
||||
axes3.set_title('Timestep 3')
|
||||
axes3.axis('off')
|
||||
plt.show(fig3)
|
||||
|
||||
# Define the Median RGB colour box
|
||||
def i(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||
fig4, axes4 = plt.subplots(figsize=(3,3))
|
||||
fig4 = plt.imshow([[(int(np.median([p1r, p2r, p3r])), int(np.median([p1g, p2g, p3g])), int(np.median([p1b, p2b, p3b])))]])
|
||||
axes4.set_title('Median RGB - All timesteps')
|
||||
axes4.axis('off')
|
||||
plt.show(fig4)
|
||||
|
||||
# Define the Geomedian RGB colour box
|
||||
def ii(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||
fig5, axes5 = plt.subplots(figsize=(3,3))
|
||||
fig5 = plt.imshow([[(int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).red.values.ravel()[0]), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).green.values.ravel()[0]), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).blue.values.ravel()[0]))]])
|
||||
axes5.set_title('Geomedian RGB - All timesteps')
|
||||
axes5.axis('off')
|
||||
plt.show(fig5)
|
||||
|
||||
# Define 3-D axis to display vectors on
|
||||
def j(p1r, p1g, p1b, p2r, p2g, p2b, p3r, p3g, p3b):
|
||||
fig6 = plt.figure()
|
||||
axes6 = fig6.add_subplot(111, projection='3d')
|
||||
x = [p1r, p2r, p3r, int(np.median([p1r, p2r, p3r])), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).red.values.ravel()[0])]
|
||||
y = [p1g, p2g, p3g, int(np.median([p1g, p2g, p3g])), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).green.values.ravel()[0])]
|
||||
z = [p1b, p2b, p3b, int(np.median([p1b, p2b, p3b])), int(xr_geomedian(xr.Dataset({"red": (("x", "y", "time"), [[[np.float32(p1r), np.float32(p2r), np.float32(p3r)]]]), "green": (("x", "y", "time"), [[[np.float32(p1g), np.float32(p2g), np.float32(p3g)]]]), "blue": (("x", "y", "time"), [[[np.float32(p1b), np.float32(p2b), np.float32(p3b)]]])})).blue.values.ravel()[0])]
|
||||
labels = [' 1', ' 2', ' 3', ' median', ' geomedian']
|
||||
axes6.scatter(x, y, z, c=['black','black','black','r', 'blue'], marker='o')
|
||||
axes6.set_xlabel('Red')
|
||||
axes6.set_ylabel('Green')
|
||||
axes6.set_zlabel('Blue')
|
||||
axes6.set_xlim3d(0, 255)
|
||||
axes6.set_ylim3d(0, 255)
|
||||
axes6.set_zlim3d(0, 255)
|
||||
for ax, ay, az, label in zip(x, y, z, labels):
|
||||
axes6.text(ax, ay, az, label)
|
||||
plt.title('Each band represents a dimension.')
|
||||
plt.show()
|
||||
|
||||
# Define outputs
|
||||
outf = widgets.interactive_output(f, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||
outg = widgets.interactive_output(g, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||
|
||||
outh = widgets.interactive_output(h, {'p1r': p1r, 'p1g': p1g, 'p1b': p1b})
|
||||
outhh = widgets.interactive_output(hh, {'p2r': p2r, 'p2g': p2g, 'p2b': p2b})
|
||||
outhhh = widgets.interactive_output(hhh, {'p3r': p3r, 'p3g': p3g, 'p3b': p3b})
|
||||
|
||||
outi = widgets.interactive_output(i, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||
outii = widgets.interactive_output(ii, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||
|
||||
outj = widgets.interactive_output(j, {'p1r': p1r, 'p2r': p2r,'p3r': p3r, 'p1g': p1g, 'p2g': p2g,'p3g': p3g, 'p1b': p1b, 'p2b': p2b,'p3b': p3b})
|
||||
|
||||
app_output = widgets.HBox([widgets.VBox([widgets.HBox([outh, widgets.VBox([ p1r, p1g, p1b])]), widgets.HBox([outhh, widgets.VBox([p2r, p2g, p2b])]), widgets.HBox([outhhh, widgets.VBox([ p3r, p3g, p3b])])]), widgets.VBox([widgets.HBox([widgets.VBox([outf, outi]), widgets.VBox([outg, outii])]), outj])])
|
||||
|
||||
return app_output
|
||||
@@ -0,0 +1,372 @@
|
||||
"""
|
||||
Create an interactive map for selecting satellite imagery and exporting image files.
|
||||
"""
|
||||
|
||||
# Load modules
|
||||
import datacube
|
||||
import itertools
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from odc.ui import select_on_a_map
|
||||
from datacube.utils.geometry import CRS
|
||||
from datacube.utils import masking
|
||||
from skimage import exposure
|
||||
from ipyleaflet import (WMSLayer, basemaps, basemap_to_tiles)
|
||||
from traitlets import Unicode
|
||||
|
||||
from deafrica_tools.spatial import reverse_geocode
|
||||
from deafrica_tools.dask import create_local_dask_cluster
|
||||
|
||||
|
||||
def select_region_app(date,
|
||||
satellites,
|
||||
size_limit=10000):
|
||||
"""
|
||||
An interactive app that allows the user to select a region from a
|
||||
map using imagery from Sentinel-2 and Landsat. The output of this
|
||||
function is used as the input to :func:`export_image_app` to export high-
|
||||
resolution satellite images.
|
||||
|
||||
Last modified: September 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
date : str
|
||||
The exact date used to plot imagery on the interactive map
|
||||
(e.g. ``date='1988-01-01'``).
|
||||
satellites : str
|
||||
The satellite data to plot on the interactive map. The
|
||||
following options are supported:
|
||||
|
||||
``'Landsat-9'``: data from the Landsat 9 satellite
|
||||
``'Landsat-8'``: data from the Landsat 8 satellite
|
||||
``'Landsat-7'``: data from the Landsat 7 satellite
|
||||
``'Landsat-5'``: data from the Landsat 5 satellite
|
||||
``'Sentinel-2'``: data from Sentinel-2A and Sentinel-2B
|
||||
``'Sentinel-2 geomedian'``: data from the Sentinel-2 annual geomedian
|
||||
|
||||
size_limit : int, optional
|
||||
An optional size limit for the area selection in sq km.
|
||||
Defaults to 10000 sq km.
|
||||
|
||||
Returns
|
||||
-------
|
||||
A dictionary containing:
|
||||
|
||||
* 'geopolygon' (defining the area to export imagery from),
|
||||
* 'date' (date used to export imagery), and
|
||||
* 'satellites' (the satellites from which to extract imagery).
|
||||
|
||||
These are passed to the :func:`export_image_app` function to export the image.
|
||||
"""
|
||||
|
||||
########################
|
||||
# Select and load data #
|
||||
########################
|
||||
|
||||
# Load DEA WMS
|
||||
class TimeWMSLayer(WMSLayer):
|
||||
time = Unicode('').tag(sync=True, o=True)
|
||||
|
||||
# WMS layers
|
||||
wms_params = {
|
||||
'Landsat-9': 'ls9_sr',
|
||||
'Landsat-8': 'ls8_sr',
|
||||
'Landsat-7': 'ls7_sr',
|
||||
'Landsat-5': 'ls5_sr',
|
||||
'Sentinel-2': 's2_l2a',
|
||||
'Sentinel-2 geomedian': 'gm_s2_annual'
|
||||
}
|
||||
|
||||
time_wms = TimeWMSLayer(url='https://ows.digitalearth.africa/',
|
||||
layers=wms_params[satellites],
|
||||
time=date,
|
||||
format='image/png',
|
||||
transparent=True,
|
||||
attribution='Digital Earth Africa')
|
||||
|
||||
# Plot interactive map to select area
|
||||
basemap = basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)
|
||||
geopolygon = select_on_a_map(height='1000px',
|
||||
layers=(
|
||||
basemap,
|
||||
time_wms,
|
||||
),
|
||||
center=(4, 20),
|
||||
zoom=4)
|
||||
|
||||
# Test size of selected area
|
||||
area = geopolygon.to_crs(crs=CRS('epsg:6933')).area / 1000000
|
||||
if area > size_limit:
|
||||
print(f'Warning: Your selected area is {area:.00f} sq km. '
|
||||
f'Please select an area of less than {size_limit} sq km.'
|
||||
f'\nTo select a smaller area, re-run the cell '
|
||||
f'above and draw a new polygon.')
|
||||
|
||||
else:
|
||||
return {'geopolygon': geopolygon,
|
||||
'date': date,
|
||||
'satellites': satellites}
|
||||
|
||||
|
||||
def export_image_app(geopolygon,
|
||||
date,
|
||||
satellites,
|
||||
style='True colour',
|
||||
resolution=None,
|
||||
vmin=0,
|
||||
vmax=2000,
|
||||
percentile_stretch=None,
|
||||
power=None,
|
||||
image_proc_funcs=None,
|
||||
output_format="jpg",
|
||||
standardise_name=False):
|
||||
"""
|
||||
Exports Digital Earth Africa satellite data as an image file
|
||||
based on the extent and time period selected using
|
||||
:func:`select_region_app`. The function supports Sentinel-2 and Landsat
|
||||
data, creating True and False colour images.
|
||||
|
||||
By default, files are named using:
|
||||
|
||||
``"<product> - <YYYY-MM-DD> - <site, state> - <description>.png"``
|
||||
|
||||
Set ``standardise_name=True`` for a machine-readable name:
|
||||
|
||||
``"<product>_<YYYY-MM-DD>_<site-state>_<description>.png"``
|
||||
|
||||
Last modified: September 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
geopolygon : datacube.utils.geometry object
|
||||
A datacube geopolygon providing the spatial bounds used to load
|
||||
satellite data.
|
||||
date : str
|
||||
The exact date used to extract imagery
|
||||
(e.g. `date='1988-01-01'`).
|
||||
satellites : str
|
||||
The satellite data to be used to extract imagery. The
|
||||
following options are supported:
|
||||
|
||||
``'Landsat-9'``: data from the Landsat 9 satellite
|
||||
``'Landsat-8'``: data from the Landsat 8 satellite
|
||||
``'Landsat-7'``: data from the Landsat 7 satellite
|
||||
``'Landsat-5'``: data from the Landsat 5 satellite
|
||||
``'Sentinel-2'``: data from Sentinel-2A and Sentinel-2B
|
||||
``'Sentinel-2 geomedian'``: data from the Sentinel-2 annual geomedian
|
||||
|
||||
style : str, optional
|
||||
The style used to produce the image. Two options are currently
|
||||
supported:
|
||||
|
||||
* ``'True colour'``: Creates a true colour image using the red,
|
||||
green and blue satellite bands
|
||||
* ``'False colour'``: Creates a false colour image using
|
||||
short-wave infrared, infrared and green satellite bands.
|
||||
The specific bands used vary between Landsat and Sentinel-2.
|
||||
|
||||
resolution : tuple, optional
|
||||
The spatial resolution to load data. By default, the tool will
|
||||
automatically set the best possible resolution depending on the
|
||||
satellites selected (i.e 30 m for Landsat, 10 m for Sentinel-2).
|
||||
Increasing this (e.g. to ``resolution=(-100, 100)``) can be useful
|
||||
for loading large spatial extents.
|
||||
vmin, vmax : int or float
|
||||
The minimum and maximum surface reflectance values used to
|
||||
clip the resulting imagery to enhance contrast.
|
||||
percentile_stretch : tuple of floats, optional
|
||||
An tuple of two floats (between 0.00 and 1.00) that can be used
|
||||
to clip the imagery to based on percentiles to get more control
|
||||
over the brightness and contrast of the image. The default is
|
||||
``None``; ``(0.02, 0.98)`` is equivelent to ``robust=True``. If this
|
||||
parameter is used, ``vmin`` and ``vmax`` will have no effect.
|
||||
power : float, optional
|
||||
Raises imagery by a power to reduce bright features and
|
||||
enhance dark features. This can add extra definition over areas
|
||||
with extremely bright features like snow, beaches or salt pans.
|
||||
image_proc_funcs : list of funcs, optional
|
||||
An optional list containing functions that will be applied to
|
||||
the output image. This can include image processing functions
|
||||
such as increasing contrast, unsharp masking, saturation etc.
|
||||
The function should take AND return a `numpy.ndarray` with
|
||||
shape ``[y, x, bands]``. If your function has parameters, you
|
||||
can pass in custom values using a lambda function, e.g.:
|
||||
``[lambda x: skimage.filters.unsharp_mask(x, radius=5, amount=0.2)]``
|
||||
output_format : str, optional
|
||||
The output file format of the image. Valid options include ``'jpg'``
|
||||
and ``'png'``. Defaults to ``'jpg'``.
|
||||
standardise_name : bool, optional
|
||||
Whether to export the image file with a machine-readable
|
||||
file name (e.g. ``<product>_<YYYY-MM-DD>_<site-state>_<description>.png``)
|
||||
"""
|
||||
|
||||
###########################
|
||||
# Set up satellite params #
|
||||
###########################
|
||||
|
||||
sat_params = {
|
||||
'Landsat-9': {
|
||||
'products': ['ls9_sr'],
|
||||
'resolution': [-30, 30],
|
||||
'styles': {
|
||||
'True colour': ['red', 'green', 'blue'],
|
||||
'False colour': ['swir_1', 'nir', 'green']
|
||||
}
|
||||
},
|
||||
'Landsat-8': {
|
||||
'products': ['ls8_sr'],
|
||||
'resolution': [-30, 30],
|
||||
'styles': {
|
||||
'True colour': ['red', 'green', 'blue'],
|
||||
'False colour': ['swir_1', 'nir', 'green']
|
||||
}
|
||||
},
|
||||
'Landsat-7': {
|
||||
'products': ['ls7_sr'],
|
||||
'resolution': [-30, 30],
|
||||
'styles': {
|
||||
'True colour': ['red', 'green', 'blue'],
|
||||
'False colour': ['swir_1', 'nir', 'green']
|
||||
}
|
||||
},
|
||||
'Landsat-5': {
|
||||
'products': ['ls5_sr'],
|
||||
'resolution': [-30, 30],
|
||||
'styles': {
|
||||
'True colour': ['red', 'green', 'blue'],
|
||||
'False colour': ['swir_1', 'nir', 'green']
|
||||
}
|
||||
},
|
||||
'Sentinel-2': {
|
||||
'products': ['s2_l2a'],
|
||||
'resolution': [-10, 10],
|
||||
'styles': {
|
||||
'True colour': ['red', 'green', 'blue'],
|
||||
'False colour': ['swir_2', 'nir_1', 'green']
|
||||
}
|
||||
},
|
||||
'Sentinel-2 geomedian': {
|
||||
'products': ['gm_s2_annual'],
|
||||
'resolution': [-10, 10],
|
||||
'styles': {
|
||||
'True colour': ['red', 'green', 'blue'],
|
||||
'False colour': ['swir_2', 'nir_1', 'green']
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
#############
|
||||
# Load data #
|
||||
#############
|
||||
|
||||
# Connect to datacube database
|
||||
dc = datacube.Datacube(app='Exporting_satellite_images')
|
||||
|
||||
# Configure local dask cluster
|
||||
client = create_local_dask_cluster(return_client=True)
|
||||
|
||||
# Create query after adjusting interval time to UTC by
|
||||
# adding a UTC offset of -10 hours.
|
||||
start_date = np.datetime64(date)
|
||||
query_params = {
|
||||
'time': (str(start_date)),
|
||||
'geopolygon': geopolygon
|
||||
}
|
||||
|
||||
# Find matching datasets
|
||||
dss = [
|
||||
dc.find_datasets(product=i, **query_params)
|
||||
for i in sat_params[satellites]['products']
|
||||
]
|
||||
dss = list(itertools.chain.from_iterable(dss))
|
||||
|
||||
# Get CRS and sensor
|
||||
crs = str(dss[0].crs)
|
||||
|
||||
if satellites == 'Sentinel-2 geomedian':
|
||||
sensor = satellites
|
||||
else:
|
||||
sensor = dss[0].metadata_doc['properties']['eo:platform'].capitalize()
|
||||
sensor = sensor[0:-1].replace('_', '-') + sensor[-1].capitalize()
|
||||
|
||||
# Use resolution if provided, otherwise use default
|
||||
if resolution:
|
||||
sat_params[satellites]['resolution'] = resolution
|
||||
|
||||
load_params = {
|
||||
'output_crs': crs,
|
||||
'resolution': sat_params[satellites]['resolution'],
|
||||
'resampling': 'bilinear'
|
||||
}
|
||||
|
||||
# Load data from datasets
|
||||
ds = dc.load(datasets=dss,
|
||||
measurements=sat_params[satellites]['styles'][style],
|
||||
group_by='solar_day',
|
||||
dask_chunks={
|
||||
'time': 1,
|
||||
'x': 3000,
|
||||
'y': 3000
|
||||
},
|
||||
**load_params,
|
||||
**query_params)
|
||||
ds = masking.mask_invalid_data(ds)
|
||||
|
||||
rgb_array = ds.isel(time=0).to_array().values
|
||||
|
||||
############
|
||||
# Plotting #
|
||||
############
|
||||
|
||||
# Create unique file name
|
||||
centre_coords = geopolygon.centroid.coords[0][::-1]
|
||||
site = reverse_geocode(coords=centre_coords)
|
||||
fname = (f"{sensor} - {date} - {site} - {style}, "
|
||||
f"{load_params['resolution'][1]} m resolution.{output_format}")
|
||||
|
||||
# Remove spaces and commas if requested
|
||||
if standardise_name:
|
||||
fname = fname.replace(' - ', '_').replace(', ',
|
||||
'-').replace(' ',
|
||||
'-').lower()
|
||||
|
||||
print(
|
||||
f'\nExporting image to {fname}.\nThis may take several minutes to complete...'
|
||||
)
|
||||
|
||||
# Convert to numpy array
|
||||
rgb_array = np.transpose(rgb_array, axes=[1, 2, 0])
|
||||
|
||||
# If percentile stretch is supplied, calculate vmin and vmax
|
||||
# from percentiles
|
||||
if percentile_stretch:
|
||||
vmin, vmax = np.nanpercentile(rgb_array, percentile_stretch)
|
||||
|
||||
# Raise by power to dampen bright features and enhance dark.
|
||||
# Raise vmin and vmax by same amount to ensure proper stretch
|
||||
if power:
|
||||
rgb_array = rgb_array**power
|
||||
vmin, vmax = vmin**power, vmax**power
|
||||
|
||||
# Rescale/stretch imagery between vmin and vmax
|
||||
rgb_rescaled = exposure.rescale_intensity(rgb_array.astype(float),
|
||||
in_range=(vmin, vmax),
|
||||
out_range=(0.0, 1.0))
|
||||
|
||||
# Apply image processing funcs
|
||||
if image_proc_funcs:
|
||||
for i, func in enumerate(image_proc_funcs):
|
||||
print(f'Applying custom function {i + 1}')
|
||||
rgb_rescaled = func(rgb_rescaled)
|
||||
|
||||
# Plot RGB
|
||||
plt.imshow(rgb_rescaled)
|
||||
|
||||
# Export to file
|
||||
plt.imsave(fname=fname, arr=rgb_rescaled, format=output_format)
|
||||
|
||||
# Close dask client
|
||||
client.shutdown()
|
||||
|
||||
print('Finished exporting image.')
|
||||
@@ -0,0 +1,388 @@
|
||||
"""
|
||||
Wetlands insight tool widget, which can be used to run an interactive
|
||||
version of the wetlands insight tool.
|
||||
"""
|
||||
|
||||
# Import required packages
|
||||
|
||||
# Force GeoPandas to use Shapely instead of PyGEOS
|
||||
# In a future release, GeoPandas will switch to using Shapely by default.
|
||||
import os
|
||||
os.environ['USE_PYGEOS'] = '0'
|
||||
|
||||
import datacube
|
||||
import warnings
|
||||
import seaborn as sns
|
||||
import matplotlib.pyplot as plt
|
||||
from datacube.utils.geometry import CRS
|
||||
from ipyleaflet import (
|
||||
WMSLayer,
|
||||
basemaps,
|
||||
basemap_to_tiles,
|
||||
Map,
|
||||
DrawControl,
|
||||
WidgetControl,
|
||||
LayerGroup,
|
||||
LayersControl,
|
||||
)
|
||||
from traitlets import Unicode
|
||||
from ipywidgets import (
|
||||
GridspecLayout,
|
||||
Button,
|
||||
Layout,
|
||||
HBox,
|
||||
VBox,
|
||||
HTML,
|
||||
Output,
|
||||
)
|
||||
import json
|
||||
import geopandas as gpd
|
||||
from io import BytesIO
|
||||
from dask.diagnostics import ProgressBar
|
||||
|
||||
import deafrica_tools
|
||||
from deafrica_tools.dask import create_local_dask_cluster
|
||||
from deafrica_tools.wetlands import WIT_drill
|
||||
import deafrica_tools.app.widgetconstructors as deawidgets
|
||||
|
||||
|
||||
def make_box_layout():
|
||||
return Layout(
|
||||
#border='solid 1px black',
|
||||
margin='0px 10px 10px 0px',
|
||||
padding='5px 5px 5px 5px',
|
||||
width='100%',
|
||||
height='100%',
|
||||
)
|
||||
|
||||
|
||||
def create_expanded_button(description, button_style):
|
||||
return Button(
|
||||
description=description,
|
||||
button_style=button_style,
|
||||
layout=Layout(width="auto", height="auto"),
|
||||
)
|
||||
|
||||
|
||||
class wit_app(HBox):
|
||||
def __init__(self, lang=None):
|
||||
super().__init__()
|
||||
|
||||
deafrica_tools.set_lang(lang)
|
||||
|
||||
##########################################################
|
||||
# INITIAL ATTRIBUTES #
|
||||
|
||||
self.startdate = "2020-01-01"
|
||||
self.enddate = "2020-03-01"
|
||||
self.mingooddata = 0.0
|
||||
self.resamplingfreq = "1M"
|
||||
self.out_csv = "example_WIT.csv"
|
||||
self.out_plot = "example_WIT.png"
|
||||
self.product_list = [
|
||||
(_("None"), "none"),
|
||||
(_("ESRI World Imagery"), "esri_world_imagery"),
|
||||
(_("Sentinel-2 Geomedian"), "gm_s2_annual"),
|
||||
(_("Water Observations from Space"), "wofs_ls_summary_annual"),
|
||||
|
||||
]
|
||||
self.product = self.product_list[0][1]
|
||||
self.product_year = "2020-01-01"
|
||||
self.target = None
|
||||
self.action = None
|
||||
self.gdf_drawn = None
|
||||
|
||||
##########################################################
|
||||
# HEADER FOR APP #
|
||||
|
||||
# Create the Header widget
|
||||
header_title_text = _("Wetlands Insight Tool")
|
||||
instruction_text = _("Select parameters and AOI")
|
||||
self.header = deawidgets.create_html(f"<h3>{header_title_text}</h3><p>{instruction_text}</p>")
|
||||
self.header.layout = make_box_layout()
|
||||
|
||||
##########################################################
|
||||
# HANDLER FUNCTION FOR DRAW CONTROL #
|
||||
|
||||
# Define the action to take once something is drawn on the map
|
||||
def update_geojson(target, action, geo_json):
|
||||
|
||||
self.action = action
|
||||
|
||||
json_data = json.dumps(geo_json)
|
||||
binary_data = json_data.encode()
|
||||
io = BytesIO(binary_data)
|
||||
io.seek(0)
|
||||
|
||||
gdf = gpd.read_file(io)
|
||||
gdf.crs = "EPSG:4326"
|
||||
self.gdf_drawn = gdf
|
||||
|
||||
gdf_drawn_epsg6933 = gdf.copy().to_crs("EPSG:6933")
|
||||
m2_per_km2 = 10 ** 6
|
||||
area = gdf_drawn_epsg6933.area.values[0] / m2_per_km2
|
||||
polyarea_label = _('Total polygon area')
|
||||
polyarea_text = f"<p><b>{polyarea_label}</b>: {area:.2f} km<sup>2</sup></p>"
|
||||
|
||||
if area <= 3000:
|
||||
confirmation_text = '<p style="color:#33cc33;">' + _('Area falls within recommended limit') + '</p>'
|
||||
self.header.value = header_title_text + polyarea_text + confirmation_text
|
||||
else:
|
||||
warning_text = '<p style="color:#ff5050;">' + _('Area is too large, please update your polygon') + '</p>'
|
||||
self.header.value = header_title_text + polyarea_text + warning_text
|
||||
|
||||
##########################################################
|
||||
# WIDGETS FOR APP OUTPUTS #
|
||||
|
||||
self.dask_client = Output(layout=make_box_layout())
|
||||
self.progress_bar = Output(layout=make_box_layout())
|
||||
self.wit_plot = Output(layout=make_box_layout())
|
||||
self.progress_header = deawidgets.create_html("")
|
||||
|
||||
##########################################################
|
||||
# MAP WIDGET, DRAWING TOOLS, WMS LAYERS #
|
||||
|
||||
# Create drawing tools
|
||||
desired_drawtools = ['rectangle', 'polygon']
|
||||
draw_control = deawidgets.create_drawcontrol(desired_drawtools)
|
||||
|
||||
# Begin by displaying an empty layer group, and update the group with desired WMS on interaction.
|
||||
self.deafrica_layers = LayerGroup(layers=())
|
||||
self.deafrica_layers.name = _('Map Overlays')
|
||||
|
||||
# Create map widget
|
||||
self.m = deawidgets.create_map()
|
||||
|
||||
self.m.layout = make_box_layout()
|
||||
|
||||
# Add tools to map widget
|
||||
self.m.add_control(draw_control)
|
||||
self.m.add_layer(self.deafrica_layers)
|
||||
|
||||
# Store current basemap for future use
|
||||
self.basemap = self.m.basemap
|
||||
|
||||
##########################################################
|
||||
# WIDGETS FOR APP CONTROLS #
|
||||
|
||||
# Create parameter widgets
|
||||
startdate_picker = deawidgets.create_datepicker()
|
||||
enddate_picker = deawidgets.create_datepicker()
|
||||
min_good_data = deawidgets.create_boundedfloattext(self.mingooddata, 0.0, 1.0, 0.05)
|
||||
resampling_freq = deawidgets.create_inputtext(self.resamplingfreq, self.resamplingfreq)
|
||||
output_csv = deawidgets.create_inputtext(self.out_csv, self.out_csv)
|
||||
output_plot = deawidgets.create_inputtext(self.out_plot, self.out_plot)
|
||||
deaoverlay_dropdown = deawidgets.create_dropdown(self.product_list, self.product_list[0][1])
|
||||
run_button = create_expanded_button(_("Run"), "info")
|
||||
|
||||
##########################################################
|
||||
# COLLECTION OF ALL APP CONTROLS #
|
||||
|
||||
parameter_selection = VBox(
|
||||
[
|
||||
HTML("<b>" + _("Map Overlay:") + "</b>"),
|
||||
deaoverlay_dropdown,
|
||||
HTML("<b>" + _("Start Date:") + "</b>"),
|
||||
startdate_picker,
|
||||
HTML("<b>" + _("End Date:") + "</b>"),
|
||||
enddate_picker,
|
||||
HTML("<b>" + _("Minimum Good Data:") + "</b>"),
|
||||
min_good_data,
|
||||
HTML("<b>" + _("Resampling Frequency:") + "</b>"),
|
||||
resampling_freq,
|
||||
HTML("<b>" + _("Output CSV:") + "</b>"),
|
||||
output_csv,
|
||||
HTML("<b>" + _("Output Plot:") + "</b>"),
|
||||
output_plot,
|
||||
]
|
||||
)
|
||||
parameter_selection.layout = make_box_layout()
|
||||
|
||||
##########################################################
|
||||
# SPECIFICATION OF APP LAYOUT #
|
||||
|
||||
# Create the layout #[rowspan, colspan]
|
||||
grid = GridspecLayout(11, 10, height="1100px", width="auto")
|
||||
|
||||
# Controls and Status
|
||||
grid[0, :] = self.header
|
||||
grid[1:6, 0:2] = parameter_selection
|
||||
grid[6, 0:2] = run_button
|
||||
|
||||
# Dask and Progress info
|
||||
grid[1, 7:] = self.dask_client
|
||||
grid[2:7, 7:] = self.progress_bar
|
||||
|
||||
# Map
|
||||
grid[1:7, 2:7] = self.m
|
||||
|
||||
# Plot
|
||||
grid[7:, :] = self.wit_plot
|
||||
|
||||
# Display using HBox children attribute
|
||||
self.children = [grid]
|
||||
|
||||
##########################################################
|
||||
# SPECIFICATION UPDATE FUNCTIONS FOR EACH WIDGET #
|
||||
|
||||
# Run update functions whenever various widgets are changed.
|
||||
startdate_picker.observe(self.update_startdate, "value")
|
||||
enddate_picker.observe(self.update_enddate, "value")
|
||||
min_good_data.observe(self.update_mingooddata, "value")
|
||||
resampling_freq.observe(self.update_resamplingfreq, "value")
|
||||
output_csv.observe(self.update_outputcsv, "value")
|
||||
output_plot.observe(self.update_outputplot, "value")
|
||||
deaoverlay_dropdown.observe(self.update_deaoverlay, "value")
|
||||
run_button.on_click(self.run_app)
|
||||
draw_control.on_draw(update_geojson)
|
||||
|
||||
##############################################################
|
||||
# DEFINITION OF ALL UPDATE FUNCTIONS #
|
||||
|
||||
# set the start date to the new edited date
|
||||
def update_startdate(self, change):
|
||||
self.startdate = change.new
|
||||
|
||||
# set the end date to the new edited date
|
||||
def update_enddate(self, change):
|
||||
self.enddate = change.new
|
||||
|
||||
# set the min good data
|
||||
def update_mingooddata(self, change):
|
||||
self.mingooddata = change.new
|
||||
|
||||
# set the resampling frequency
|
||||
def update_resamplingfreq(self, change):
|
||||
self.resamplingfreq = change.new
|
||||
|
||||
# set the output csv
|
||||
def update_outputcsv(self, change):
|
||||
self.out_csv = change.new
|
||||
|
||||
# set the output plot
|
||||
def update_outputplot(self, change):
|
||||
self.out_plot = change.new
|
||||
|
||||
# Update product
|
||||
def update_deaoverlay(self, change):
|
||||
|
||||
self.product = change.new
|
||||
|
||||
if self.product == "none":
|
||||
self.deafrica_layers.clear_layers()
|
||||
elif self.product == "esri_world_imagery":
|
||||
self.deafrica_layers.clear_layers()
|
||||
layer = basemap_to_tiles(basemaps.Esri.WorldImagery)
|
||||
self.deafrica_layers.add_layer(layer)
|
||||
else:
|
||||
self.deafrica_layers.clear_layers()
|
||||
layer = deawidgets.create_dea_wms_layer(self.product, self.product_year)
|
||||
self.deafrica_layers.add_layer(layer)
|
||||
|
||||
def run_app(self, change):
|
||||
|
||||
# Clear progress bar and output areas before running
|
||||
self.dask_client.clear_output()
|
||||
self.progress_bar.clear_output()
|
||||
self.wit_plot.clear_output()
|
||||
|
||||
# Connect to datacube database
|
||||
dc = datacube.Datacube(app="wetland_app")
|
||||
|
||||
# Configure local dask cluster
|
||||
with self.dask_client:
|
||||
client = create_local_dask_cluster(
|
||||
return_client=True, display_client=True
|
||||
)
|
||||
|
||||
# Set any defaults
|
||||
TCW_threshold = -0.035
|
||||
dask_chunks = dict(x=1000, y=1000, time=1)
|
||||
|
||||
#check resampling freq
|
||||
if self.resamplingfreq == 'None':
|
||||
rsf = None
|
||||
else:
|
||||
rsf = self.resamplingfreq
|
||||
|
||||
self.progress_header.value = f"<h3>"+_("Progress")+"</h3>"
|
||||
|
||||
# run wetlands polygon drill
|
||||
with self.progress_bar:
|
||||
# with ProgressBar():
|
||||
warnings.filterwarnings("ignore")
|
||||
try:
|
||||
df = WIT_drill(
|
||||
gdf=self.gdf_drawn,
|
||||
time=(self.startdate, self.enddate),
|
||||
min_gooddata=self.mingooddata,
|
||||
resample_frequency=rsf,
|
||||
TCW_threshold=TCW_threshold,
|
||||
export_csv=self.out_csv,
|
||||
dask_chunks=dask_chunks,
|
||||
verbose=False,
|
||||
verbose_progress=True,
|
||||
)
|
||||
print(_("WIT complete"))
|
||||
except AttributeError:
|
||||
print(_("No polygon selected"))
|
||||
|
||||
# close down the dask client
|
||||
client.shutdown()
|
||||
|
||||
# save the csv
|
||||
if self.out_csv:
|
||||
df.to_csv(self.out_csv, index_label="Datetime")
|
||||
|
||||
# ---Plotting------------------------------
|
||||
|
||||
with self.wit_plot:
|
||||
|
||||
fontsize = 17
|
||||
plt.rcParams.update({"font.size": fontsize})
|
||||
# set up color palette
|
||||
pal = [
|
||||
sns.xkcd_rgb["cobalt blue"],
|
||||
sns.xkcd_rgb["neon blue"],
|
||||
sns.xkcd_rgb["grass"],
|
||||
sns.xkcd_rgb["beige"],
|
||||
sns.xkcd_rgb["brown"],
|
||||
]
|
||||
|
||||
# make a stacked area plot
|
||||
plt.close("all")
|
||||
|
||||
fig, ax = plt.subplots(constrained_layout=True, figsize=(20, 6))
|
||||
|
||||
ax.stackplot(
|
||||
df.index,
|
||||
df.wofs_area_percent,
|
||||
df.wet_percent,
|
||||
df.green_veg_percent,
|
||||
df.dry_veg_percent,
|
||||
df.bare_soil_percent,
|
||||
labels=[
|
||||
_("open water"),
|
||||
_("wet"),
|
||||
_("green veg"),
|
||||
_("dry veg"),
|
||||
_("bare soil"),
|
||||
],
|
||||
colors=pal,
|
||||
alpha=0.6,
|
||||
)
|
||||
|
||||
# set axis limits to the min and max
|
||||
ax.set_ylim(0, 100)
|
||||
ax.set_xlim(df.index[0], df.index[-1])
|
||||
ax.tick_params(axis="x", labelsize=fontsize)
|
||||
|
||||
# add a legend and a tight plot box
|
||||
ax.legend(loc="lower left", framealpha=0.6)
|
||||
ax.set_title(_("Percentage Fractional Cover, Wetness, and Water"))
|
||||
# plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
if self.out_plot:
|
||||
# save the figure
|
||||
fig.savefig(f"{self.out_plot}")
|
||||
@@ -0,0 +1,367 @@
|
||||
"""
|
||||
Functions for easily defining widgets in the context of DE Africa notebooks.
|
||||
|
||||
These are largely customised wrappers around existing widgets.
|
||||
"""
|
||||
|
||||
import ipyleaflet as leaflet
|
||||
from ipyleaflet import LayersControl
|
||||
import ipywidgets as widgets
|
||||
from traitlets import Unicode
|
||||
|
||||
|
||||
def create_datepicker(description='', value=None, layout={'width': '85%'}):
|
||||
'''
|
||||
Create a DatePicker widget
|
||||
|
||||
Last modified: July 2022
|
||||
|
||||
Parameters
|
||||
----------
|
||||
description : string
|
||||
descirption label to attach
|
||||
layout : dictionary
|
||||
any layout commands for the widget
|
||||
|
||||
Returns
|
||||
-------
|
||||
date_picker : ipywidgets.widgets.widget_date.DatePicker
|
||||
|
||||
'''
|
||||
|
||||
date_picker = widgets.DatePicker(
|
||||
description=description,
|
||||
layout=layout,
|
||||
disabled=False,
|
||||
value=value
|
||||
)
|
||||
|
||||
return date_picker
|
||||
|
||||
|
||||
def create_inputtext(value, placeholder, description="", layout={'width': '85%'}):
|
||||
'''
|
||||
Create a Text widget
|
||||
|
||||
Last modified: October 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : string
|
||||
initial value of the widget
|
||||
placeholder : string
|
||||
placeholder text to display to the user before intput
|
||||
description : string
|
||||
descirption label to attach
|
||||
layout : dictionary
|
||||
any layout commands for the widget
|
||||
|
||||
Returns
|
||||
-------
|
||||
input_text : ipywidgets.widgets.widget_string.Text
|
||||
|
||||
'''
|
||||
|
||||
input_text = widgets.Text(
|
||||
value=value,
|
||||
placeholder=placeholder,
|
||||
description=description,
|
||||
layout=layout,
|
||||
disabled=False
|
||||
)
|
||||
|
||||
return input_text
|
||||
|
||||
|
||||
def create_boundedfloattext(value, min_val, max_val, step_val, description="", layout={'width': '85%'}):
|
||||
'''
|
||||
Create a BoundedFloatText widget
|
||||
|
||||
Last modified: October 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : float
|
||||
initial value of the widget
|
||||
min_val : float
|
||||
minimum allowed value for the float
|
||||
max_val : float
|
||||
maximum allowed value for the float
|
||||
step_val : float
|
||||
allowed increment for the float
|
||||
description : string
|
||||
descirption label to attach
|
||||
layout : dictionary
|
||||
any layout commands for the widget
|
||||
|
||||
Returns
|
||||
-------
|
||||
float_text : ipywidgets.widgets.widget_float.BoundedFloatText
|
||||
|
||||
'''
|
||||
|
||||
float_text = widgets.BoundedFloatText(
|
||||
value=value,
|
||||
min=min_val,
|
||||
max=max_val,
|
||||
step=step_val,
|
||||
description=description,
|
||||
layout=layout,
|
||||
disabled=False,
|
||||
)
|
||||
|
||||
return float_text
|
||||
|
||||
|
||||
def create_dropdown(options, value, description="", layout={'width': '85%'}):
|
||||
'''
|
||||
Create a Dropdown widget
|
||||
|
||||
Last modified: October 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
options : list
|
||||
a list of options for the user to select from
|
||||
value : string
|
||||
initial value of the widget
|
||||
description : string
|
||||
descirption label to attach
|
||||
layout : dictionary
|
||||
any layout commands for the widget
|
||||
|
||||
Returns
|
||||
-------
|
||||
dropdown : ipywidgets.widgets.widget_selection.Dropdown
|
||||
|
||||
'''
|
||||
|
||||
dropdown = widgets.Dropdown(
|
||||
options=options,
|
||||
value=value,
|
||||
description=description,
|
||||
layout=layout,
|
||||
disabled=False,
|
||||
)
|
||||
|
||||
return dropdown
|
||||
|
||||
|
||||
def create_html(value):
|
||||
'''
|
||||
Create a HTML widget
|
||||
|
||||
Last modified: October 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : string
|
||||
HTML text to display
|
||||
|
||||
Returns
|
||||
-------
|
||||
html : ipywidgets.widgets.widget_string.HTML
|
||||
|
||||
'''
|
||||
|
||||
html = widgets.HTML(
|
||||
value=value,
|
||||
)
|
||||
|
||||
return html
|
||||
|
||||
|
||||
def create_map(map_center=(4, 20), zoom_level=3, basemap=leaflet.basemaps.OpenStreetMap.Mapnik, basemap_name='Open Street Map'):
|
||||
'''
|
||||
Create an interactive ipyleaflet map
|
||||
|
||||
Last modified: October 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
map_center : tuple
|
||||
A tuple containing the latitude and longitude to focus on.
|
||||
Defaults to center of Africa, (4, 20)
|
||||
zoom_level : integer
|
||||
Zoom level for the map
|
||||
Defaults to 3 to view all of Africa
|
||||
basemap : ipyleaflet basemap (dict)
|
||||
Basemap to use, can be any from https://ipyleaflet.readthedocs.io/en/latest/api_reference/basemaps.html
|
||||
Defaults to Open Street Map (basemaps.OpenStreetMap.Mapnik)
|
||||
basemap_name : string
|
||||
Layer name for the basemap
|
||||
|
||||
Returns
|
||||
-------
|
||||
m : ipyleaflet.leaflet.Map
|
||||
interactive ipyleaflet map
|
||||
|
||||
'''
|
||||
|
||||
basemap_tiles = leaflet.basemap_to_tiles(basemap)
|
||||
basemap_tiles.name = basemap_name
|
||||
|
||||
m = leaflet.Map(center=map_center, zoom=zoom_level, basemap=basemap_tiles, scroll_wheel_zoom=True)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
def create_dea_wms_layer(product, date):
|
||||
'''
|
||||
Create a Digital Earth Africa WMS layer to add to a map
|
||||
|
||||
Last modified: October 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
product : string
|
||||
The Digital Earth Africa product to load
|
||||
(e.g. 'gm_s2_annual')
|
||||
date : string (yyyy-mm-dd format)
|
||||
The date to load the product for
|
||||
|
||||
Returns
|
||||
-------
|
||||
time_wms : ipyleaflet WMS layer
|
||||
|
||||
'''
|
||||
|
||||
|
||||
# Load DEA WMS
|
||||
class TimeWMSLayer(leaflet.WMSLayer):
|
||||
time = Unicode("").tag(sync=True, o=True)
|
||||
|
||||
time_wms = TimeWMSLayer(
|
||||
url="https://ows.digitalearth.africa/",
|
||||
layers=product,
|
||||
time=date,
|
||||
format="image/png",
|
||||
transparent=True,
|
||||
attribution="Digital Earth Africa",
|
||||
)
|
||||
|
||||
return time_wms
|
||||
|
||||
|
||||
def create_drawcontrol(
|
||||
draw_controls = ['rectangle', 'polygon', 'circle', 'polyline', 'marker', 'circlemarker'],
|
||||
rectangle_options={},
|
||||
polygon_options={},
|
||||
circle_options={},
|
||||
polyline_options={},
|
||||
marker_options={},
|
||||
circlemarker_options={},
|
||||
):
|
||||
'''
|
||||
Create a draw control widget to add to ipyleaflet maps
|
||||
|
||||
Last modified: October 2021
|
||||
|
||||
Parameters
|
||||
----------
|
||||
draw_controls : list
|
||||
List of draw controls to add to the map. Defaults to adding all
|
||||
Viable options are 'rectangle', 'polygon', 'circle', 'polyline', 'marker', 'circlemarker'
|
||||
rectangle_options : dict
|
||||
Options to customise the appearence of the relevant shape
|
||||
User can supply, or leave blank to get default DE Africa appearence
|
||||
polygon_options : dict
|
||||
Options to customise the appearence of the relevant shape
|
||||
User can supply, or leave blank to get default DE Africa appearence
|
||||
circle_options : dict
|
||||
Options to customise the appearence of the relevant shape
|
||||
User can supply, or leave blank to get default DE Africa appearence
|
||||
polyline_options : dict
|
||||
Options to customise the appearence of the relevant shape
|
||||
User can supply, or leave blank to get default DE Africa appearence
|
||||
marker_options : dict
|
||||
Options to customise the appearence of the relevant shape
|
||||
User can supply, or leave blank to get default DE Africa appearence
|
||||
circlemarker_options : dict
|
||||
Options to customise the appearence of the relevant shape
|
||||
User can supply, or leave blank to get default DE Africa appearence
|
||||
|
||||
|
||||
Returns
|
||||
-------
|
||||
draw_control : ipyleaflet.leaflet.DrawControl
|
||||
|
||||
'''
|
||||
|
||||
# Set defualt DE Africa styling options for polygons
|
||||
default_shapeoptions = {
|
||||
"color": "#FFFFFF",
|
||||
"opacity": 0.8,
|
||||
"fillColor": "#336699",
|
||||
"fillOpacity": 0.4,
|
||||
}
|
||||
default_drawerror = {
|
||||
"color": "#FF6633",
|
||||
"message": "Drawing error, clear all and try again"
|
||||
}
|
||||
|
||||
# Set draw control appearence to DE Africa defaults
|
||||
# Do this if user has requested a control, but has not provided a corresponding options dict
|
||||
|
||||
if ('rectangle' in draw_controls) and (not rectangle_options):
|
||||
rectangle_options = {"shapeOptions": default_shapeoptions}
|
||||
|
||||
if ('polygon' in draw_controls) and (not polygon_options):
|
||||
polygon_options = {
|
||||
"shapeOptions": default_shapeoptions,
|
||||
"drawError": default_drawerror,
|
||||
"allowIntersection": False,
|
||||
}
|
||||
|
||||
if ('circle' in draw_controls) and (not circle_options):
|
||||
circle_options = {"shapeOptions": default_shapeoptions}
|
||||
|
||||
if ('polyline' in draw_controls) and (not polyline_options):
|
||||
polyline_options = {"shapeOptions": default_shapeoptions}
|
||||
|
||||
if ('marker' in draw_controls) and (not marker_options):
|
||||
marker_options = {'shapeOptions': {'opacity': 1.0}}
|
||||
|
||||
if ('circlemarker' in draw_controls) and (not circlemarker_options):
|
||||
circlemarker_options = {"shapeOptions": default_shapeoptions}
|
||||
|
||||
# Instantiate draw control and add options
|
||||
draw_control = leaflet.DrawControl()
|
||||
draw_control.rectangle = rectangle_options
|
||||
draw_control.polygon = polygon_options
|
||||
draw_control.marker = marker_options
|
||||
draw_control.circle = circle_options
|
||||
draw_control.circlemarker = circlemarker_options
|
||||
draw_control.polyline = polyline_options
|
||||
|
||||
return draw_control
|
||||
|
||||
|
||||
def create_checkbox(value, description="", layout={'width': '85%'}):
|
||||
'''
|
||||
Create a Checkbox widget
|
||||
|
||||
Last modified: July 2022
|
||||
|
||||
Parameters
|
||||
----------
|
||||
value : string
|
||||
initial value of the widget; True or False
|
||||
description : string
|
||||
description label to attach
|
||||
layout : dictionary
|
||||
any layout commands for the widget
|
||||
|
||||
Returns
|
||||
-------
|
||||
dropdown : ipywidgets.widgets.widget_selection.Dropdown
|
||||
|
||||
'''
|
||||
|
||||
checklist = widgets.Checkbox(value=value,
|
||||
description=description,
|
||||
layout=layout,
|
||||
disabled=False,
|
||||
indent=False)
|
||||
|
||||
return checklist
|
||||
Reference in New Issue
Block a user