1026 lines
37 KiB
Python
1026 lines
37 KiB
Python
'''
|
||
Functions for loading and interacting with Global Forest Change data in the forest monitoring notebook, inside the Real_world_examples folder.
|
||
'''
|
||
|
||
# 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 json
|
||
import warnings
|
||
from io import BytesIO
|
||
|
||
import deafrica_tools.app.widgetconstructors as deawidgets
|
||
import geopandas as gpd
|
||
import ipywidgets as widgets
|
||
import matplotlib.colors as mcolors
|
||
import matplotlib.pyplot as plt
|
||
import numpy as np
|
||
import pandas as pd
|
||
import rioxarray
|
||
import xarray as xr
|
||
from deafrica_tools.dask import create_local_dask_cluster
|
||
from deafrica_tools.spatial import xr_rasterize
|
||
from ipyleaflet import (
|
||
DrawControl,
|
||
GeoData,
|
||
LayerGroup,
|
||
LayersControl,
|
||
Map,
|
||
WidgetControl,
|
||
WMSLayer,
|
||
basemap_to_tiles,
|
||
basemaps,
|
||
)
|
||
from ipywidgets import HTML, Button, GridspecLayout, HBox, Layout, Output, VBox
|
||
from matplotlib.patches import Patch
|
||
from traitlets import Unicode
|
||
|
||
# Turn off all warnings.
|
||
warnings.filterwarnings("ignore")
|
||
warnings.simplefilter("ignore")
|
||
|
||
def make_box_layout():
|
||
"""
|
||
Defines a number of CSS properties that impact how a widget is laid out.
|
||
"""
|
||
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):
|
||
"""
|
||
Defines a number of CSS properties to create a button to handle mouse clicks.
|
||
"""
|
||
return Button(
|
||
description=description,
|
||
button_style=button_style,
|
||
layout=Layout(width="auto", height="auto"),
|
||
)
|
||
|
||
def load_gfclayer(gdf_drawn, gfclayer):
|
||
"""
|
||
Loads the selected Global Forest Change layer for the
|
||
area drawn on the map widget.
|
||
"""
|
||
# Configure local dask cluster.
|
||
client = create_local_dask_cluster(return_client=True, display_client=True)
|
||
|
||
# Get the coordinates of the top-left corner for each Global Forest Change tile,
|
||
# covering the area of interest.
|
||
min_lat, max_lat = (
|
||
gdf_drawn.bounds.miny.item(),
|
||
gdf_drawn.bounds.maxy.item(),
|
||
)
|
||
min_lon, max_lon = (
|
||
gdf_drawn.bounds.minx.item(),
|
||
gdf_drawn.bounds.maxx.item(),
|
||
)
|
||
|
||
lats = np.arange(
|
||
np.floor(min_lat / 10) * 10, np.ceil(max_lat / 10) * 10, 10
|
||
).astype(int)
|
||
lons = np.arange(
|
||
np.floor(min_lon / 10) * 10, np.ceil(max_lon / 10) * 10, 10
|
||
).astype(int)
|
||
|
||
coord_list = []
|
||
for lat in lats:
|
||
lat = lat + 10
|
||
if lat >= 0:
|
||
lat_str = f"{lat:02d}N"
|
||
else:
|
||
lat_str = f"{abs(lat):02d}S"
|
||
for lon in lons:
|
||
if lon >= 0:
|
||
lon_str = f"{lon:03d}E"
|
||
else:
|
||
lon_str = f"{abs(lon):03d}W"
|
||
coord_str = f"{lat_str}_{lon_str}"
|
||
coord_list.append(coord_str)
|
||
|
||
# Load each Global Forest Change tile covering the area of interest.
|
||
base_url = f"https://storage.googleapis.com/earthenginepartners-hansen/GFC-2021-v1.9/Hansen_GFC-2021-v1.9_{gfclayer}_"
|
||
dask_chunks = dict(x=2048, y=2048)
|
||
|
||
tile_list = []
|
||
for coord in coord_list:
|
||
tile_url = f"{base_url}{coord}.tif"
|
||
# Load the tile as an xarray.DataArray.
|
||
tile = rioxarray.open_rasterio(tile_url, chunks=dask_chunks).squeeze()
|
||
tile_list.append(tile)
|
||
|
||
# Merge the tiles into a single xarray.DataArray.
|
||
ds = xr.combine_by_coords(tile_list)
|
||
# Clip the dataset using the bounds of the area of interest.
|
||
ds = ds.rio.clip_box(
|
||
minx=min_lon - 0.00025,
|
||
miny=min_lat - 0.00025,
|
||
maxx=max_lon + 0.00025,
|
||
maxy=max_lat + 0.00025,
|
||
)
|
||
# Rename the y and x variables for DEA convention on xarray.DataArrays where crs="EPSG:4326".
|
||
ds = ds.rename({"y": "latitude", "x": "longitude"})
|
||
|
||
# Mask pixels representing no loss (encoded as 0) in the "lossyear" layer.
|
||
if gfclayer == "lossyear":
|
||
ds = ds.where(ds != 0)
|
||
# Mask pixels representing no gain (encoded as 0) in the "gain" layer.
|
||
elif gfclayer == "gain":
|
||
ds = ds.where(ds != 0)
|
||
# Mask pixels with 0 percentage tree canopy cover.
|
||
elif gfclayer == "treecover2000":
|
||
ds = ds.where(ds != 0)
|
||
|
||
# Create a mask from the area of interest GeoDataFrame.
|
||
mask = xr_rasterize(gdf_drawn, ds)
|
||
# Mask the dataset.
|
||
ds = ds.where(mask)
|
||
# Convert the xarray.DataArray to a dataset.
|
||
ds = ds.to_dataset(name=gfclayer)
|
||
# Compute.
|
||
ds = ds.compute()
|
||
# Assign the "EPSG:4326" CRS to the dataset.
|
||
ds.rio.write_crs(4326, inplace=True)
|
||
ds = ds.transpose("latitude", "longitude")
|
||
|
||
# Close down the dask client.
|
||
client.close()
|
||
return ds
|
||
|
||
def load_all_gfclayers(gdf_drawn):
|
||
gfclayers = ["treecover2000", "gain", "lossyear"]
|
||
|
||
dataset_list = []
|
||
for layer in gfclayers:
|
||
ds = load_gfclayer(gdf_drawn, gfclayer=layer)
|
||
dataset_list.append(ds)
|
||
|
||
dataset = xr.merge(dataset_list)
|
||
return dataset
|
||
|
||
def get_gfclayer_treecover2000(gfclayer_ds, gfclayer="treecover2000"):
|
||
"""
|
||
Preprocess the Global Forest change "treecover2020" layer.
|
||
"""
|
||
ds = gfclayer_ds[gfclayer]
|
||
|
||
# Check if the dataarray is empty.
|
||
condition = ds.isnull().all().item()
|
||
|
||
if condition:
|
||
return None
|
||
else:
|
||
# Mask the dataset.
|
||
mask = np.isnan(ds)
|
||
ds_masked = ds.where(mask, 1)
|
||
|
||
# Get the pixel count for each unique pixel value in the layer.
|
||
counts = np.unique(ds_masked, return_counts=True)
|
||
# Remove the counts for pixels with the value np.nan.
|
||
index = np.argwhere(np.isnan(counts[0]))
|
||
counts_dict = dict(
|
||
zip(np.delete(counts[0], index), np.delete(counts[1], index))
|
||
)
|
||
|
||
# Reproject the dataset to EPSG:6933 which uses metres
|
||
ds_reprojected = ds_masked.rio.reproject("EPSG:6933")
|
||
# Get the area per pixel.
|
||
pixel_length = ds_reprojected.geobox.resolution[1]
|
||
m_per_km = 1000
|
||
per_pixel_area = (pixel_length / m_per_km) ** 2
|
||
|
||
# Save the results as a pandas DataFrame.
|
||
df = pd.DataFrame(
|
||
data={
|
||
"Year": ["2000"],
|
||
"Tree Cover in km$^2$": np.fromiter(counts_dict.values(), dtype=float)
|
||
* per_pixel_area,
|
||
}
|
||
)
|
||
|
||
# Get the total area.
|
||
print_statement = f'Total Forest Cover in {df["Year"].item()}: {round(df["Tree Cover in km$^2$"].item(), 4)} km2'
|
||
|
||
# File name to use when exporting results.
|
||
file_name = f"forest_cover_in_2000"
|
||
|
||
return ds, df, print_statement, file_name
|
||
|
||
def get_gfclayer_gain(gfclayer_ds, gfclayer="gain"):
|
||
"""
|
||
Preprocess the Global Forest Change "gain" layer.
|
||
"""
|
||
ds = gfclayer_ds[gfclayer]
|
||
|
||
# Check if the dataarray is empty.
|
||
condition = ds.isnull().all().item()
|
||
|
||
if condition:
|
||
return None
|
||
else:
|
||
# Get the pixel count for each unique pixel value in the layer.
|
||
counts = np.unique(ds, return_counts=True)
|
||
# Remove the counts for pixels with the value np.nan.
|
||
index = np.argwhere(np.isnan(counts[0]))
|
||
counts_dict = dict(
|
||
zip(np.delete(counts[0], index), np.delete(counts[1], index))
|
||
)
|
||
|
||
# Reproject the dataset to EPSG:6933 which uses metres.
|
||
ds_reprojected = ds.rio.reproject("EPSG:6933")
|
||
# Get the area per pixel.
|
||
pixel_length = ds_reprojected.geobox.resolution[1]
|
||
m_per_km = 1000
|
||
per_pixel_area = (pixel_length / m_per_km) ** 2
|
||
|
||
# Save the results as a pandas DataFrame.
|
||
df = pd.DataFrame(
|
||
data={
|
||
"Year": ["2000-2012"],
|
||
"Forest Cover Gain in km$^2$": np.fromiter(
|
||
counts_dict.values(), dtype=float
|
||
)
|
||
* per_pixel_area,
|
||
}
|
||
)
|
||
|
||
# Get the total area.
|
||
print_statement = f'Total Forest Cover Gain {df["Year"].item()}: {round(df["Forest Cover Gain in km$^2$"].item(), 4)} km2'
|
||
|
||
# File name to use when exporting results.
|
||
file_name = f"forest_cover_gain_from_2000_to_2012"
|
||
|
||
return ds, df, print_statement, file_name
|
||
|
||
def get_gfclayer_lossyear(gfclayer_ds, start_year, end_year, gfclayer="lossyear"):
|
||
"""
|
||
Preprocess the Global Forest Change "lossyear" layer.
|
||
"""
|
||
|
||
ds = gfclayer_ds[gfclayer]
|
||
|
||
# Mask the dataset to the selected time range.
|
||
selected_years = list(range(start_year, end_year + 1))
|
||
mask = ds.isin(selected_years)
|
||
ds = ds.where(mask)
|
||
|
||
# Check if the dataarray is empty.
|
||
condition = ds.isnull().all().item()
|
||
|
||
if condition:
|
||
return None
|
||
else:
|
||
# Get the pixel count for each unique pixel value in the layer.
|
||
counts = np.unique(ds, return_counts=True)
|
||
# Remove the counts for pixels with the value np.nan.
|
||
index = np.argwhere(np.isnan(counts[0]))
|
||
counts_dict = dict(
|
||
zip(np.delete(counts[0], index), np.delete(counts[1], index))
|
||
)
|
||
|
||
# Reproject the dataset to EPSG:6933 which uses metres
|
||
ds_reprojected = ds.rio.reproject("EPSG:6933")
|
||
# Get the area per pixel.
|
||
pixel_length = ds_reprojected.geobox.resolution[1]
|
||
m_per_km = 1000
|
||
per_pixel_area = (pixel_length / m_per_km) ** 2
|
||
|
||
# For each year get the area of loss.
|
||
# Save the results as a pandas DataFrame.
|
||
df = pd.DataFrame(
|
||
{
|
||
"Year": 2000 + np.fromiter(counts_dict.keys(), dtype=int),
|
||
"Forest Cover Loss in km$^2$": np.fromiter(
|
||
counts_dict.values(), dtype=float
|
||
)
|
||
* per_pixel_area,
|
||
}
|
||
)
|
||
|
||
# Get the total area.
|
||
print_statement = f'Total Forest Cover Loss from {start_year + 2000} to {end_year + 2000}: {round(df["Forest Cover Loss in km$^2$"].sum(), 4)} km2'
|
||
|
||
# File name to use when exporting results.
|
||
file_name = f"forest_cover_loss_from_{start_year + 2000}_to_{end_year + 2000}"
|
||
|
||
return ds, df, print_statement, file_name
|
||
|
||
def plot_gfclayer_treecover2000(gfclayer_ds, gfclayer="treecover2000"):
|
||
"""
|
||
Plot the Global Forest Change "treecover2000" layer.
|
||
"""
|
||
|
||
if get_gfclayer_treecover2000(gfclayer_ds) is None:
|
||
print(
|
||
f"No Global Forest Change {gfclayer} layer data found in the selected area. Please select a new polygon over an area with data."
|
||
)
|
||
else:
|
||
ds, df, print_statement, file_name = get_gfclayer_treecover2000(gfclayer_ds)
|
||
|
||
# Export the dataframe as a csv.
|
||
df.to_csv(f"{file_name}.csv", index=False)
|
||
print(f'Table exported to "{file_name}.csv"')
|
||
|
||
# Define the plotting parameters.
|
||
figure_width = 10
|
||
figure_length = 10
|
||
title = f"Tree Canopy Cover for the Year 2000"
|
||
|
||
# Plot the dataset.
|
||
fig, ax = plt.subplots(figsize=(figure_width, figure_length))
|
||
im = ds.plot(cmap="Greens", add_colorbar=False, ax=ax)
|
||
# Add a colorbar to the plot.
|
||
cbar = plt.colorbar(mappable=im)
|
||
cbar.set_label(
|
||
"Percentage tree canopy cover for year 2000", labelpad=-65, y=0.25
|
||
)
|
||
# Add a title to the plot.
|
||
plt.title(title)
|
||
# Save the plot.
|
||
plt.savefig(f"{file_name}.png")
|
||
print(f'Figure exported to "{file_name}.png"')
|
||
plt.show()
|
||
|
||
print(print_statement)
|
||
|
||
def plot_gfclayer_gain(gfclayer_ds, gfclayer="gain"):
|
||
"""
|
||
Plot the Global Forest Change "gain" layer.
|
||
"""
|
||
|
||
if get_gfclayer_gain(gfclayer_ds) is None:
|
||
print(
|
||
f"No Global Forest Change {gfclayer} layer data found in the selected area. Please select a new polygon over an area with data."
|
||
)
|
||
else:
|
||
ds, df, print_statement, file_name = get_gfclayer_gain(gfclayer_ds)
|
||
|
||
# Export the dataframe as a csv.
|
||
df.to_csv(f"{file_name}.csv", index=False)
|
||
print(f'Table exported to "{file_name}.csv"')
|
||
|
||
# Define the plotting parameters.
|
||
color = "#6CAE75"
|
||
figure_width = 10
|
||
figure_length = 10
|
||
title = f"Forest Cover Gain from 2000 to 2012"
|
||
|
||
# Plot the dataset.
|
||
fig, ax = plt.subplots(figsize=(figure_width, figure_length))
|
||
im = ds.plot(cmap=mcolors.ListedColormap([color]), add_colorbar=False, ax=ax)
|
||
# Add a legend to the plot.
|
||
im.axes.legend(
|
||
[Patch(facecolor=color)],
|
||
["Global forest cover gain 2000–2012"],
|
||
loc="lower left",
|
||
bbox_to_anchor=(1.0, 0.5),
|
||
frameon=False,
|
||
)
|
||
# Add a title to the plot.
|
||
plt.title(title)
|
||
# Save the plot.
|
||
plt.savefig(f"{file_name}.png")
|
||
print(f'Figure exported to "{file_name}.png"')
|
||
plt.show()
|
||
|
||
print(print_statement)
|
||
|
||
def plot_gfclayer_lossyear(gfclayer_ds, start_year, end_year, gfclayer="lossyear"):
|
||
"""
|
||
Plot the Global Forest change "lossyear" layer.
|
||
"""
|
||
|
||
if (
|
||
get_gfclayer_lossyear(gfclayer_ds, start_year, end_year, gfclayer="lossyear")
|
||
is None
|
||
):
|
||
print(
|
||
f"No Global Forest Change {gfclayer} layer data found in the selected area. Please select a new polygon over an area with data."
|
||
)
|
||
else:
|
||
ds, df, print_statement, file_name = get_gfclayer_lossyear(
|
||
gfclayer_ds, start_year, end_year, gfclayer="lossyear"
|
||
)
|
||
|
||
# Export the dataframe as a csv.
|
||
df.to_csv(f"{file_name}.csv", index=False)
|
||
print(f'Table exported to "{file_name}.csv"')
|
||
|
||
# Define the plotting parameters.
|
||
figure_width = 10
|
||
figure_length = 15
|
||
nrows = 2
|
||
ncols = 1
|
||
title = f"Forest Cover Loss from {start_year + 2000} to {end_year + 2000}"
|
||
|
||
# Location of transition from one color to the next on the colormap.
|
||
color_levels = list(np.arange(1 - 0.5, 22, 1))
|
||
# Ticks to be displayed.
|
||
ticks = list(np.arange(1, 22))
|
||
tick_labels = list(2000 + np.arange(1, 22))
|
||
|
||
# Define the color map to use when plotting.
|
||
color_list = [
|
||
"#e6194b",
|
||
"#3cb44b",
|
||
"#ffe119",
|
||
"#4363d8",
|
||
"#f58231",
|
||
"#911eb4",
|
||
"#46f0f0",
|
||
"#f032e6",
|
||
"#bcf60c",
|
||
"#fabebe",
|
||
"#008080",
|
||
"#e6beff",
|
||
"#9a6324",
|
||
"#fffac8",
|
||
"#800000",
|
||
"#aaffc3",
|
||
"#808000",
|
||
"#ffd8b1",
|
||
"#000075",
|
||
"#808080",
|
||
"#7A306C",
|
||
]
|
||
cmap = mcolors.ListedColormap(colors=color_list, N=21)
|
||
norm = mcolors.BoundaryNorm(boundaries=color_levels, ncolors=cmap.N)
|
||
|
||
# Plot the dataset.
|
||
fig, (ax1, ax2) = plt.subplots(
|
||
nrows, ncols, figsize=(figure_width, figure_length)
|
||
)
|
||
im = ds.plot(ax=ax1, cmap=cmap, norm=norm, add_colorbar=False)
|
||
# Add a title to the subplot.
|
||
ax1.set_title(title)
|
||
# Add a colorbar to the subplot.
|
||
cbar = plt.colorbar(mappable=im, ticks=ticks)
|
||
cbar.set_label("Year of gross forest cover loss event", labelpad=-60, y=0.25)
|
||
cbar.set_ticklabels(tick_labels)
|
||
# Plot the second subplot.
|
||
df.plot(
|
||
x="Year",
|
||
y="Forest Cover Loss in km$^2$",
|
||
ylabel="Forest Cover Loss in km$^2$",
|
||
title=title,
|
||
ax=ax2,
|
||
)
|
||
# Save the plot.
|
||
plt.savefig(f"{file_name}.png")
|
||
print(f'Figure exported to "{file_name}.png"')
|
||
plt.show()
|
||
|
||
print(print_statement)
|
||
|
||
def plot_gfclayer_all(gfclayer_ds, start_year, end_year):
|
||
"""
|
||
Plot all the Global Forest Change Layers loaded.
|
||
"""
|
||
|
||
# Define the plotting parameters.
|
||
figure_width = 10
|
||
figure_length = 10
|
||
treecover_color = "Greens"
|
||
gain_color = "yellow"
|
||
lossyear_color = "red"
|
||
|
||
print_statement_list = []
|
||
filename_list = ["\nTables exported as: "]
|
||
|
||
figure_fn = "global_forest_change_all_layers.png"
|
||
|
||
# Define the figure.
|
||
fig, ax = plt.subplots(figsize=(figure_width, figure_length))
|
||
if (
|
||
get_gfclayer_treecover2000(
|
||
gfclayer_ds[["treecover2000"]], gfclayer="treecover2000"
|
||
)
|
||
is None
|
||
):
|
||
print(
|
||
f"No Global Forest Change 'treecover2000' layer data found in the selected area. Please select a new polygon over an area with data."
|
||
)
|
||
else:
|
||
(
|
||
ds_treecover2000,
|
||
df_treecover2000,
|
||
print_statement_treecover2000,
|
||
file_name_treecover2000,
|
||
) = get_gfclayer_treecover2000(
|
||
gfclayer_ds[["treecover2000"]], gfclayer="treecover2000"
|
||
)
|
||
# Plot the treecover2000 layer as the background layer.
|
||
background = ds_treecover2000.plot(
|
||
cmap=treecover_color, add_colorbar=False, ax=ax
|
||
)
|
||
# Add a colorbar to the treecover2000 plot.
|
||
cbar = plt.colorbar(mappable=background)
|
||
cbar.set_label(
|
||
"Percentage tree canopy cover for year 2000", labelpad=-65, y=0.25
|
||
)
|
||
# Export the dataframe as a csv.
|
||
df_treecover2000.to_csv(f"{file_name_treecover2000}.csv", index=False)
|
||
# Add the print statement to the list.
|
||
print_statement_list.append(print_statement_treecover2000)
|
||
# Add the file name to the list.
|
||
filename_list.append(f'"{file_name_treecover2000}.csv"')
|
||
|
||
if get_gfclayer_gain(gfclayer_ds[["gain"]], gfclayer="gain") is None:
|
||
print(
|
||
f"No Global Forest Change 'gain' layer data found in the selected area. Please select a new polygon over an area with data."
|
||
)
|
||
else:
|
||
ds_gain, df_gain, print_statement_gain, file_name_gain = get_gfclayer_gain(
|
||
gfclayer_ds[["gain"]], gfclayer="gain"
|
||
)
|
||
# Plot the gain layer.
|
||
ds_gain.plot(
|
||
ax=ax, cmap=mcolors.ListedColormap([gain_color]), add_colorbar=False
|
||
)
|
||
# Export the dataframe as a csv.
|
||
df_gain.to_csv(f"{file_name_gain}.csv", index=False)
|
||
# Add the print statement to the list.
|
||
print_statement_list.append(print_statement_gain)
|
||
# Add the file name to the list.
|
||
filename_list.append(f'"{file_name_gain}.csv"')
|
||
|
||
if (
|
||
get_gfclayer_lossyear(
|
||
gfclayer_ds[["lossyear"]], start_year, end_year, gfclayer="lossyear"
|
||
)
|
||
is None
|
||
):
|
||
print(
|
||
f"No Global Forest Change 'lossyear' layer data found in the selected area. Please select a new polygon over an area with data."
|
||
)
|
||
else:
|
||
(
|
||
ds_lossyear,
|
||
df_lossyear,
|
||
print_statement_lossyear,
|
||
file_name_lossyear,
|
||
) = get_gfclayer_lossyear(
|
||
gfclayer_ds[["lossyear"]], start_year, end_year, gfclayer="lossyear"
|
||
)
|
||
# Plot the lossyear layer.
|
||
ds_lossyear.plot(
|
||
ax=ax, cmap=mcolors.ListedColormap([lossyear_color]), add_colorbar=False
|
||
)
|
||
# Export the dataframe as a csv.
|
||
df_lossyear.to_csv(f"{file_name_lossyear}.csv", index=False)
|
||
# Add the print statement to the list.
|
||
print_statement_list.append(print_statement_lossyear)
|
||
# Add the file name to the list.
|
||
filename_list.append(f'"{file_name_lossyear}.csv"')
|
||
|
||
# Add a legend to the plot.
|
||
ax.legend(
|
||
[Patch(facecolor=gain_color), Patch(facecolor=lossyear_color)],
|
||
[
|
||
"Global forest cover \n gain 2000–2012",
|
||
f"Global forest cover \n loss {str(2000+start_year)}-{str(2000+end_year)}",
|
||
],
|
||
loc="lower right",
|
||
bbox_to_anchor=(-0.1, 0.75),
|
||
frameon=False,
|
||
)
|
||
|
||
plt.title("Global Forest Change Layers")
|
||
plt.savefig(figure_fn)
|
||
plt.show()
|
||
print(*print_statement_list, sep="\n")
|
||
print(*filename_list, sep="\n\t")
|
||
print(f'\nFigure saved as "{figure_fn}"');
|
||
|
||
def plot_gfclayer(gfclayer_ds, start_year, end_year, gfclayer):
|
||
if gfclayer == "treecover2000":
|
||
plot_gfclayer_treecover2000(gfclayer_ds, gfclayer)
|
||
elif gfclayer == "lossyear":
|
||
plot_gfclayer_lossyear(gfclayer_ds, start_year, end_year, gfclayer)
|
||
elif gfclayer == "gain":
|
||
plot_gfclayer_gain(gfclayer_ds, gfclayer)
|
||
elif gfclayer == "alllayers":
|
||
plot_gfclayer_all(gfclayer_ds, start_year, end_year)
|
||
|
||
def update_map_layers(self):
|
||
"""
|
||
Updates map widget to add new basemap when selected
|
||
using menu options.
|
||
"""
|
||
# Clear data load parameters to trigger data reload.
|
||
self.gfclayer_ds = None
|
||
|
||
# Remove all layers from the map_layers Layers Group.
|
||
self.map_layers.clear_layers()
|
||
# Add the selected basemap to the layer Group.
|
||
self.map_layers.add_layer(self.basemap)
|
||
|
||
class forest_monitoring_app(HBox):
|
||
def __init__(self):
|
||
super().__init__()
|
||
|
||
##################
|
||
# HEADER FOR APP #
|
||
##################
|
||
|
||
# Create the header widget.
|
||
header_title_text = "<h3>Digital Earth Africa Forest Change</h3>"
|
||
instruction_text = """<p>Select the desired Global Forest Change layer, then zoom in and draw a polygon to
|
||
select an area for which to plot the selected Global Forest Change layer. Alternatively, <b>upload a vector file</b> of the area of interest.</p>"""
|
||
self.header = deawidgets.create_html(
|
||
value=f"{header_title_text}{instruction_text}"
|
||
)
|
||
self.header.layout = make_box_layout()
|
||
|
||
############################
|
||
# WIDGETS FOR APP CONTROLS #
|
||
############################
|
||
|
||
## Selection widget for selecting the basemap to use for the map widget.
|
||
## and when plotting the Global Forest Change Layer.
|
||
# Basemaps available for selection for the map widget.
|
||
self.basemap_list = [
|
||
("Open Street Map", basemap_to_tiles(basemaps.OpenStreetMap.Mapnik)),
|
||
("ESRI World Imagery", basemap_to_tiles(basemaps.Esri.WorldImagery)),
|
||
]
|
||
# Set the default basemap to be used for the map widget / initial value for the widget.
|
||
self.basemap = self.basemap_list[0][1]
|
||
# Dropdown selection widget.
|
||
dropdown_basemap = deawidgets.create_dropdown(
|
||
options=self.basemap_list, value=self.basemap
|
||
)
|
||
# Register the update function to run when a new value is selected
|
||
# on the dropdown_basemap widget.
|
||
dropdown_basemap.observe(self.update_basemap, "value")
|
||
# Text to accompany the dropdown selection widget.
|
||
basemap_selection_html = deawidgets.create_html(
|
||
value=f"</br><b>Map overlay:</b>"
|
||
)
|
||
# Combine the basemap_selection_html text and the dropdown_basemap widget in a single container.
|
||
basemap_selection = VBox([basemap_selection_html, dropdown_basemap])
|
||
|
||
## Selection widget for selecting the Global Forest change layer to plot.
|
||
# Global Forest Change layers available plotting.
|
||
self.gfclayers_list = [
|
||
("Year of gross forest cover loss event", "lossyear"),
|
||
("Global forest cover gain 2000–2012", "gain"),
|
||
("Tree canopy cover for the year 2000", "treecover2000"),
|
||
("All layers", "alllayers"),
|
||
]
|
||
# Set the default GFC layer to be plotted / initial value for the widget.
|
||
self.gfclayer = self.gfclayers_list[0][1]
|
||
|
||
## Selection widget for the data time range.
|
||
# Set the default time range for which to load data for.
|
||
self.start_year = 1
|
||
self.end_year = 21
|
||
|
||
# Create the time range selector.
|
||
time_range = list(range(self.start_year, self.end_year + 1))
|
||
time_range_str = [str(2000 + i) for i in time_range]
|
||
|
||
timerange_options = tuple(zip(time_range_str, time_range))
|
||
timerange_selection_slide = widgets.SelectionRangeSlider(
|
||
options=timerange_options,
|
||
value=(self.start_year, self.end_year),
|
||
description="",
|
||
disabled=False,
|
||
)
|
||
# Register the update function to run when a new value is selected on the slider.
|
||
timerange_selection_slide.observe(self.update_timerange, "value")
|
||
# Text to accompany the timerange_selection widget.
|
||
timerange_selection_html = deawidgets.create_html(
|
||
value=f"</br><b>Forest Cover Loss Time Range:</b>"
|
||
)
|
||
# Combine the timerange_selection_text and the timerange_selection_slide in a single container.
|
||
timerange_selection = VBox(
|
||
[timerange_selection_html, timerange_selection_slide]
|
||
)
|
||
|
||
# Set the initial parameter for the GFC layer dataset.
|
||
self.gfclayer_ds = None
|
||
# Dropdown selection widget.
|
||
dropdown_gfclayer = deawidgets.create_dropdown(
|
||
options=self.gfclayers_list, value=self.gfclayer
|
||
)
|
||
# Register the update function to run when a new value is selected
|
||
# on the dropdown_gfclayer widget.
|
||
dropdown_gfclayer.observe(self.update_gfclayer, "value")
|
||
# Text to accompany the dropdown selection widget.
|
||
gfclayer_selection_html = deawidgets.create_html(
|
||
value=f"</br><b>Global Forest Change Layer:</b>"
|
||
)
|
||
# Combine the gfclayer_selection_html text and the dropdown_gfclayer widget in a single container.
|
||
gfclayer_selection = VBox([gfclayer_selection_html, dropdown_gfclayer])
|
||
|
||
## Add a checkbox for whether to overide the limit to the size of polygon drawn on the
|
||
## map widget.
|
||
# Initial value of the widget.
|
||
self.max_size = False
|
||
# CheckBox widget.
|
||
checkbox_max_size = deawidgets.create_checkbox(
|
||
value=self.max_size, description="Enable", layout={"width": "95%"}
|
||
)
|
||
# Text to accompany the CheckBox widget.
|
||
checkbox_max_size_html = deawidgets.create_html(
|
||
value=f"""</br><b>Override maximum size limit:
|
||
(use with caution; may cause memory issues/crashes)<b>"""
|
||
)
|
||
# Register the update function to run when the checkbox is ticked.
|
||
# on the checkbox_max_size CheckBox
|
||
checkbox_max_size.observe(self.update_checkbox_max_size, "value")
|
||
# # Combine the checkbox_max_size_html text and the checkbox_max_size widget in a single container.
|
||
enable_max_size = VBox([checkbox_max_size_html, checkbox_max_size])
|
||
|
||
# Add widget to enable uploading a geojson or ESRI shapefile.
|
||
self.gdf_uploaded = None
|
||
fileupload_aoi = widgets.FileUpload(accept="", multiple=True)
|
||
# Register the update function to be called for the file upload.
|
||
fileupload_aoi.observe(self.update_fileupload_aoi, "value")
|
||
fileupload_html = deawidgets.create_html(value=f"""</br><i><b>Advanced</b></br>Upload a GeoJSON or ESRI Shapefile (<5 mb) containing a single area of interest.</i>""")
|
||
fileupload = VBox([fileupload_html, fileupload_aoi])
|
||
|
||
|
||
## Put the app controls widgets into a single container.
|
||
parameter_selection = VBox(
|
||
[
|
||
basemap_selection,
|
||
gfclayer_selection,
|
||
timerange_selection,
|
||
enable_max_size,
|
||
fileupload
|
||
]
|
||
)
|
||
parameter_selection.layout = make_box_layout()
|
||
|
||
## Button to click to run the app.
|
||
run_button = create_expanded_button(
|
||
description="Generate plot", button_style="info"
|
||
)
|
||
# Register the update function to be called when the run_button button
|
||
# is clicked.
|
||
run_button.on_click(self.run_app)
|
||
|
||
|
||
|
||
###########################
|
||
# WIDGETS FOR APP OUTPUTS #
|
||
###########################
|
||
|
||
self.status_info = Output(layout=make_box_layout())
|
||
self.output_plot = Output(layout=make_box_layout())
|
||
|
||
#################################
|
||
# MAP WIDGET WITH DRAWING TOOLS #
|
||
#################################
|
||
|
||
# Create the map widget.
|
||
self.m = deawidgets.create_map(
|
||
map_center=(-18.45, 28.93),
|
||
zoom_level=11,
|
||
)
|
||
self.m.layout = make_box_layout()
|
||
|
||
# Create an empty Layer Group.
|
||
self.map_layers = LayerGroup(layers=())
|
||
# Name of the Layer Group layer.
|
||
self.map_layers.name = "Map Overlays"
|
||
# Add the empty Layer Group as a single layer to the map widget.
|
||
self.m.add_layer(self.map_layers)
|
||
|
||
# Create the desired drawing tools.
|
||
desired_drawtools = ["rectangle", "polygon"]
|
||
draw_control = deawidgets.create_drawcontrol(desired_drawtools)
|
||
# Add drawing tools to the map widget.
|
||
self.m.add_control(draw_control)
|
||
# Set the initial parameters for the drawing tools.
|
||
self.target = None
|
||
self.action = None
|
||
self.gdf_drawn = None
|
||
|
||
#####################################
|
||
# HANDLER FUNCTION FOR DRAW CONTROL #
|
||
#####################################
|
||
|
||
def handle_draw(target, action, geo_json):
|
||
|
||
"""
|
||
Defines the action to take once something is drawn on the
|
||
map widget.
|
||
"""
|
||
# Remove previously uploaded data if present
|
||
self.gdf_uploaded = None
|
||
fileupload_aoi._counter = 0
|
||
|
||
self.target = target
|
||
self.action = action
|
||
|
||
# Clear data load parameters to trigger data reload.
|
||
self.gfclayer_ds = None
|
||
|
||
# Convert the drawn polygon geojson to a GeoDataFrame.
|
||
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 the GeoDataFrame to WGS 84 / NSIDC EASE-Grid 2.0 Global and compute the 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 = (
|
||
f"Total area of Global Forest Change {self.gfclayer} layer to load"
|
||
)
|
||
polyarea_text = f"<b>{polyarea_label}</b>: {area:.2f} ha</sup>"
|
||
|
||
# Test the size of the polygon drawn.
|
||
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 )</b></span>"""
|
||
self.header.value = (
|
||
header_title_text + instruction_text + polyarea_text + warning_text
|
||
)
|
||
self.gdf_drawn = None
|
||
|
||
# Register the handler for draw events.
|
||
draw_control.on_draw(handle_draw)
|
||
|
||
###############################
|
||
# SPECIFICATION OF APP LAYOUT #
|
||
###############################
|
||
|
||
# Create the app layout.
|
||
grid_rows = 12
|
||
grid_columns = 11
|
||
grid_height = "1500px"
|
||
grid_width = "auto"
|
||
grid = GridspecLayout(
|
||
grid_rows, grid_columns, height=grid_height, width=grid_width
|
||
)
|
||
|
||
# Place app widgets and components in app layout.
|
||
# [rows, columns]
|
||
grid[0, :] = self.header
|
||
grid[1:6, 0:4] = parameter_selection
|
||
grid[6, 0:4] = run_button
|
||
grid[7:, 0:4] = self.status_info
|
||
grid[6:, 4:] = self.output_plot
|
||
grid[1:6, 4:] = self.m
|
||
# Display using HBox children attribute
|
||
self.children = [grid]
|
||
|
||
######################################
|
||
# DEFINITION OF ALL UPDATE FUNCTIONS #
|
||
######################################
|
||
|
||
def update_basemap(self, change):
|
||
"""
|
||
Updates the basemap on the map widget based on the
|
||
selected value of the dropdown_basemap widget.
|
||
"""
|
||
self.basemap = change.new
|
||
self.output_plot_basemap = get_basemap(self.basemap.url)
|
||
update_map_layers(self)
|
||
|
||
def update_gfclayer(self, change):
|
||
"""
|
||
Updates the Global Forest Change layer to be plotted
|
||
based on the selected value of the dropdown_gfclayer widget.
|
||
"""
|
||
self.gfclayer = change.new
|
||
|
||
def update_timerange(self, change):
|
||
"""Updates the time range of the data to be loaded"""
|
||
self.start_year = change.new[0]
|
||
self.end_year = change.new[1]
|
||
|
||
def update_checkbox_max_size(self, change):
|
||
"""
|
||
Sets the value of self.max_size to True when the
|
||
checkbox_max_size CheckBox is checked.
|
||
"""
|
||
self.max_size = change.new
|
||
|
||
def update_fileupload_aoi(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]
|
||
aoi_gdf = (gpd.read_file(valid_file).to_crs(
|
||
"EPSG:4326").explode().reset_index(drop=True))
|
||
|
||
# Create a geodata
|
||
geodata = GeoData(geo_dataframe=aoi_gdf,
|
||
style={
|
||
'color': 'black',
|
||
'weight': 3
|
||
})
|
||
|
||
# Add to map
|
||
xmin, ymin, xmax, ymax = aoi_gdf.total_bounds
|
||
self.m.fit_bounds([[ymin, xmin], [ymax, xmax]])
|
||
self.m.add_layer(geodata)
|
||
|
||
# If completed, add to attribute
|
||
self.gdf_uploaded = aoi_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
|
||
|
||
def run_app(self, change):
|
||
|
||
# Clear progress bar and output areas before running.
|
||
self.status_info.clear_output()
|
||
self.output_plot.clear_output()
|
||
|
||
with self.status_info:
|
||
# Load the area of interest from the map or uploaded files.
|
||
if self.gdf_uploaded is not None:
|
||
aoi_gdf = self.gdf_uploaded
|
||
elif self.gdf_drawn is not None:
|
||
aoi_gdf = self.gdf_drawn
|
||
else:
|
||
print(f'No valid polygon drawn on the map or uploaded. Please draw a valid a transect on the map, or upload a GeoJSON or ESRI Shapefile.',
|
||
end='\r')
|
||
aoi_gdf = None
|
||
|
||
# If valid area of interest data returned. Load the selected Global Forest Change data.
|
||
if aoi_gdf is not None:
|
||
|
||
if self.gfclayer_ds is None:
|
||
if self.gfclayer != "alllayers":
|
||
self.gfclayer_ds = load_gfclayer(gdf_drawn=aoi_gdf, gfclayer=self.gfclayer)
|
||
else:
|
||
self.gfclayer_ds = load_all_gfclayers(gdf_drawn=aoi_gdf)
|
||
else:
|
||
print("Using previously loaded data")
|
||
|
||
# Plot the selected Global Forest Change layer.
|
||
if self.gfclayer_ds is not None:
|
||
with self.output_plot:
|
||
plot_gfclayer(gfclayer_ds=self.gfclayer_ds,
|
||
start_year=self.start_year,
|
||
end_year=self.end_year,
|
||
gfclayer=self.gfclayer)
|
||
else:
|
||
with self.status_info:
|
||
print(f"No Global Forest Change {self.gfclayer} layer data found in the selected area. Please select a new polygon over an area with data.") |