# -*- 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".'
                )