''' 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 = "
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, upload a vector file of the area of interest.
""" 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"Map overlay:" ) # 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"Forest Cover Loss Time Range:" ) # 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"Global Forest Change Layer:" ) # 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"""Override maximum size limit: (use with caution; may cause memory issues/crashes)""" ) # 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"""AdvancedUpload a GeoJSON or ESRI Shapefile (<5 mb) containing a single area of interest.""") 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"{polyarea_label}: {area:.2f} ha" # Test the size of the polygon drawn. if self.max_size: confirmation_text = """ (Overriding maximum size limit; use with caution as may lead to memory issues)""" self.header.value = ( header_title_text + instruction_text + polyarea_text + confirmation_text ) self.gdf_drawn = gdf elif area <= 50000: confirmation_text = """ (Area to extract falls within recommended 50000 ha limit)""" self.header.value = ( header_title_text + instruction_text + polyarea_text + confirmation_text ) self.gdf_drawn = gdf else: warning_text = """ (Area to extract is too large, please select an area less than 50000 )""" 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.")