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".'
|
||||
)
|
||||
Reference in New Issue
Block a user