# -*- coding: utf-8 -*-
"""
Satellite imagery animation widget, which can be used to interactively
produce animations for multiple DE Africa products.
"""
import datetime
import itertools
import json
import warnings
from io import BytesIO

import datacube
import geopandas as gpd
import ipywidgets as widgets
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from datacube.utils.geometry import Geometry
from datacube.utils.masking import mask_invalid_data
from ipyleaflet import LayerGroup, basemap_to_tiles, basemaps
from ipywidgets import HTML, Button, GridspecLayout, HBox, Layout, Output, VBox
from skimage.exposure import rescale_intensity
from skimage.filters import unsharp_mask

import deafrica_tools.app.widgetconstructors as deawidgets
from deafrica_tools.coastal import get_coastlines
from deafrica_tools.dask import create_local_dask_cluster
from deafrica_tools.datahandling import load_ard
from deafrica_tools.plotting import xr_animation
from deafrica_tools.spatial import reverse_geocode

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

        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"
            )

        funcs_list = [
            rescale_intensity,
            lambda x: unsharp_mask(
                x, radius=self.unsharp_mask_radius, amount=self.unsharp_mask_amount
            ),
        ]
    else:
        funcs_list = None

    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):
    # 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))

    if len(deacl_gdf.index) > 0:
        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()
        return deacl_gdf
    else:
        print(f"No annual shorelines data available for bounding box {bounds}")
        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 to produce smooth, cloud-free animations",
            layout={"width": "90%", "height": "4em"},
        )
        text_rolling_median_window = widgets.IntText(
            value=20,
            step=1,
            description="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 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".')
