'''
Created on 04-Jul-2021

@author: bthakur
'''

import inspect
import traceback
import os
from pathlib import Path
from itertools import cycle

from xv.km.ui import SvgHelper
from xv.util.data import CacheUtil
from xv.util import strip_source_code_tabs, get_function_source_code
import matplotlib.pyplot as plt
import pandas as pd

from ._data_provider import DataProvider

class GeoPlotter(DataProvider):
    '''
    Class to plot GeoSpatial data
    '''
    _DEFAULT_FIGSIZE = (15, 10)
    
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        
        self._cmaps = [
                       "Set1", 
                       "Set2", 
                       "jet",
                       #"Set3"
                      ]
        pass
    
    def plot_df_interactive(self, 
                            df, 
                            column_name = 'name',
                            categorical = True, 
                            legend = False,
                            tooltip = "name",
                            search = None,
                            map_name = None,
                            html_file_path = None,
                            use_cache = False
                            ):
        '''
        df = a geopandas dataframe
        '''
        
        if df is None:
            self._log(f"Input df is None")
            return None
        
        if search:
            if isinstance(search, str):
                search = [search]
                
            df = self._search_df(df, 
                                column_name, 
                                searchStrings = search, 
                                superset = None, 
                                return_columns = None)
            if df is None:
                self._log(f"Search not found in dataframe: {search}")
                return None
            pass

        self._log(f"Plotting sdf.shape = {df.shape}")
        self._log(f"List of available columns = {df.columns}")
        
        if not map_name:
            map_name = "Custom Interactive Map"

        try:      
            interactive_map = df.explore(
                 column = column_name,
                 categorical = categorical,
                 legend = legend,
                 tooltip = tooltip,
                 name = map_name
            )
        
            if html_file_path is None:
                map_name = map_name.replace(" ", "_")
                html_file_path = f"out/custom_{map_name}.html"
                
            created_file_path = self._save_interactive_plot(interactive_map, html_file_path)
            interactive_map = None
            
            if created_file_path:
                return html_file_path
            return None
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:{e}
            failed to do interactive plotting
            """
            self._log(sError)
            self._log(traceback.format_exc())
    
        return None
        pass
    
    def _plot_interactive_search_result(self, 
                                        data,
                                        html_file_path = None):

        try:
            flm = self._get_folium()
       
            superset_df = self.get_superset_df()
            
            interactive_map = superset_df.boundary.explore(
                 scheme="naturalbreaks",  # use mapclassify's natural breaks scheme
                 legend = False, 
                 #k = 10, # use 10 bins
                 name="base_boundary" # name of the layer in the map
            )
            
            cmaps = cycle(self._cmaps)

            for k, df in data.items():
                if df is None:
                    continue
                
                if df.shape[0] <= 0:
                    self._log(f"The records in df is zero: {df.shape[0]}")
                    continue
                
                df = df.to_crs(superset_df.crs)
                
                column = 'name'
                categorical = True
        
                if k == "rivers_and_lakes":
                    style_kwds = {'linewidth': 3}
                else:
                    style_kwds = {}
                    
                if k == "cities":
                    marker_kwds = dict(radius=10, fill=True)
                else:
                    marker_kwds = {}
                    
                
                # changed by b thakur Sep 22, 2022
                #commented 
                '''
                df_plot = self._plot_interactive_df(df, 
                                                    interactive_map, 
                                                    categorical = categorical, 
                                                    column = column, 
                                                    style_kwds = style_kwds,
                                                    marker_kwds = marker_kwds,
                                                    cmap = next(cmaps),
                                                    )
                '''
                
                from xv.util import run_with_timeout
                timeout = 5 #sec
                
                thread_name = f'Thread_{self.__class__.__name__}.{inspect.stack()[0][3]}'
                method_to_call = self._plot_interactive_df
                method_params = (df, 
                                interactive_map, 
                                    {'categorical': categorical, 
                                     'column': column, 
                                     'style_kwds': style_kwds,
                                     'marker_kwds': marker_kwds,
                                     'cmap': next(cmaps),
                                    }
                                )
                
                df_plot = run_with_timeout(thread_name, 
                                          method_to_call, 
                                          method_params = method_params,
                                          timeout = timeout,
                                          verbose = self.verbose)
            
                
                pass #for k, df in data.items():
    
    
            #flm.TileLayer('Cities', control=True).add_to(interactive_map)  # use folium to add alternative tiles
            flm.LayerControl().add_to(interactive_map)  # use folium to add layer control
            
            if html_file_path is None:
                html_file_path = "out/world_and_cities_interactive_map.html"
            created_file_path = self._save_interactive_plot(interactive_map, html_file_path)
    
    
            interactive_map = None
            
            if created_file_path:
                return html_file_path
            return None
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:{e}
            failed to do interactive plotting
            """
            self._log(sError)
            self._log(traceback.format_exc())
    
        return None
        pass

    def _plot_interactive_df(self,
                            df,
                            interactive_map,
                            categorical = True,
                            column = 'name',
                            cmap="Set2", 
                            tooltip="name", 
                            tooltip_kwds = dict(labels=False), 
                            legend_kwds = {'loc': 'lower left'},
                            style_kwds = {},
                            marker_kwds = {},
                           ):
        try:
            df.explore(
                 m=interactive_map, # pass the map object
                 categorical = categorical,
                 column = column,
                 cmap = cmap, 
                 tooltip = tooltip,
                 tooltip_kwds = tooltip_kwds,
                 legend_kwds = legend_kwds,
                 style_kwds = style_kwds,
                 marker_kwds = marker_kwds,
                 name = column,
            )
            return True
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:{e}
            failed to do interactive plot of df
            """
            self._log(sError)
            self._log(traceback.format_exc())
    
        return None

        pass

    def _plot_search_result(self, data):
        try:
            plt.ioff()
            #world = self.get_world_df()
            superset_df = self.get_superset_df()
            
            
            grid_rows = self._get_grid_rows(data)
            
            
            fig = plt.figure(figsize = (GeoPlotter._DEFAULT_FIGSIZE[0], 
                                        0.7 * grid_rows * GeoPlotter._DEFAULT_FIGSIZE[1]))
            gs = fig.add_gridspec(grid_rows,1)
            
            #create axes and plot world boundry
            axes = []
            for row_idx in range(gs.nrows):
                ax = fig.add_subplot(gs[row_idx, 0])
                ax.set_aspect('equal')
    
                axes.append(ax)
                pass
    
            #plot background for first subplot
            superset_df.boundary.plot(ax = axes[0])  

            
    
            cmaps = cycle(self._cmaps)

            
            grid_count = 0
            for k, df in data.items():
                if df is None:
                    continue
                
                if df.shape[0] <= 0:
                    self._log(f"The records in df is zero: {df.shape[0]}")
                    continue
                
                df = df.to_crs(superset_df.crs)
                
                column = 'name'
                categorical = True
        
                df.plot(ax = axes[0],
                        column = column, 
                        categorical = categorical, 
                        cmap = "Set2",
                        ) 
                
                #create a new subplot
                grid_count = grid_count + 1
    
                #background
                superset_df.boundary.plot(ax = axes[grid_count])  
                
                if k == "rivers_and_lakes":
                    linewidth = 3
                else:
                    linewidth = 1
                
                #data
                self._plot_df(df, 
                            axes[grid_count], 
                            column = column, 
                            categorical = categorical,
                            legend = True,
                            legend_kwds = None,
                            markersize = 300,
                            linewidth = linewidth,
                            cmap = next(cmaps)
                            )
                pass
            
            fig.tight_layout()
            return SvgHelper.GET_SVG_STR(close_plot =True)
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:{e}
            failed to plot search result
            """
            self._log(sError)
            self._log(traceback.format_exc())
    
        return None
        pass


    def _plot_df(self, 
               df, 
               ax, 
               column = 'name', 
               categorical = True,
               legend = True,
               legend_kwds = None,
               markersize = 150,
               linewidth = 1, 
               cmap = "Set1",
               ):  
        
        try: 
            if legend_kwds is None:
                legend_kwds = {'loc': 'center left', 
                                 'bbox_to_anchor':(1,0.5),
                                 'fontsize': 16,
                                }
            
            df_plot = df.plot(ax = ax,
                                column = column, 
                                categorical = categorical, 
                                legend = legend, 
                                legend_kwds = legend_kwds,
                                markersize = markersize,
                                cmap = cmap,
                                linewidth = linewidth,
                                ) 
            
            ax.set_aspect('equal')
            return df_plot
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:{e}
            failed to plot df
            """
            self._log(sError)
            self._log(traceback.format_exc())
    
        return None
        pass
        
    
    def _plot_name_column(self, countries):
        plt.ioff()
        
        world = self.get_world_df()
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        world.boundary.plot(ax=ax)  
        countries.plot(ax = ax,
                        column = 'name', 
                        categorical=True, 
                        markersize = 300,
                        cmap="Set1", 
                        legend=True, 
                        legend_kwds={'loc': 'center left', 
                                     'bbox_to_anchor':(1,0.5),
                                     'fontsize': 16,
                                    }
                        ) 
        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass

    def _get_grid_rows(self, data):
        grid_rows = 1
        for k,v in data.items():
            if not v is None:
                if v.shape[0] > 0:
                    grid_rows = grid_rows + 1
            pass
            
        return grid_rows
        pass



    def plot_superset_boundry_map(self, use_cache = False):
        plot_code, svg_str = self._get_cache_or_plot(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_superset_boundry_map
                                )

        return svg_str
        pass

    def _plot_superset_boundry_map(self):
        plt.ioff()
        
        superset = self.get_superset_df()
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        superset.boundary.plot(ax=ax)  
        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass




    def plot_world_map(self, boundry = False, use_cache = False):
        if boundry:
            plot_code, svg_str = self._get_cache_or_plot(
                                    use_cache = use_cache, 
                                    calling_method = inspect.stack()[0][3],
                                    method_to_call = self._plot_world_boundary_map
                                    )
        else:
            plot_code, svg_str = self._get_cache_or_plot(
                                    use_cache = use_cache, 
                                    calling_method = inspect.stack()[0][3],
                                    method_to_call = self._plot_world_map
                                    )

        world = self.get_world_df()

        html_file_path = 'out/world_map.html'
        data = {
            'countries': world
            }
        html_file_path = self._plot_interactive_search_result(data,
                                                              html_file_path = html_file_path)
        return plot_code, svg_str, html_file_path
        pass


    def _plot_world_map(self):
        plt.ioff()
        
        world = self.get_world_df()
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        world.plot(ax=ax, legend=True, cmap="Set1")  
        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass

    def _plot_world_boundary_map(self):
        plt.ioff()
        
        world = self.get_world_df()
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        world.boundary.plot(ax=ax)  
        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass


    def plot_world_map_with_cities(self, boundry = False, use_cache = False):
        plot_code, svg_str = self._get_cache_or_plot(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_world_map_with_cities
                                )
        
        world = self.get_world_df()
        cities = self.get_cities_df()
        cities = cities.to_crs(world.crs)

        html_file_path = 'out/world_map_with_cities.html'
        data = {
            'cities': cities
            }

        html_file_path = self._plot_interactive_search_result(data,
                                    html_file_path = html_file_path)
        return plot_code, svg_str, html_file_path
        pass

    def _plot_world_map_with_cities(self):
        plt.ioff()
        
        world = self.get_world_df()
        cities = self.get_cities_df()
        cities = cities.to_crs(world.crs)
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        
        world.plot(ax=ax, color='white', edgecolor='black')
        cities.plot(ax=ax, marker='o', color='red', markersize = 10);

        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass

    def plot_world_map_with_geographic_lines(self, boundry = False, use_cache = False):
        plot_code, svg_str = self._get_cache_or_plot(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_world_map_with_geographic_lines
                                )
        
        world = self.get_world_df()
        geographic_lines = self.get_geographic_lines_df()
        geographic_lines = geographic_lines.to_crs(world.crs)

        html_file_path = 'out/world_map_with_geographic_lines.html'
        
        data = {
            'geographic_lines': geographic_lines
            }

        
        html_file_path = self._plot_interactive_search_result(data,
                                                              html_file_path = html_file_path)
        return plot_code, svg_str, html_file_path
        pass

    def _plot_world_map_with_geographic_lines(self):
        plt.ioff()
        
        world = self.get_world_df()
        geographic_lines = self.get_geographic_lines_df()
        geographic_lines = geographic_lines.to_crs(world.crs)
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        
        world.plot(ax=ax, color='white', edgecolor='black')
        geographic_lines.plot(ax=ax, marker='o', color='red', markersize = 10);

        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass


    def plot_elevation_points(self, data = None, use_cache = False):
        '''
        data must be a dictionary if not None
        '''
        
        if data is None:
            superset = self.get_superset_df()
            data = {'superset': superset}
            
        elevation_points = self.get_filtered_elevation_points(data)
        
        
        svg_str = self._plot_elevation_points(data, elevation_points)

        flm = self._get_folium()

        interactive_map = superset.boundary.explore(
             scheme="naturalbreaks",  # use mapclassify's natural breaks scheme
             legend = False, 
             #k = 10, # use 10 bins
             name="base_boundary" # name of the layer in the map
        )

        marker_kwds = dict(radius=10, fill=True)
        df_plot = self._plot_interactive_df(elevation_points, 
                                            interactive_map, 
                                            categorical = False, 
                                            column = 'elevation', 
                                            marker_kwds = marker_kwds,
                                            )
        

        #flm.TileLayer('Elevation Points', control=True).add_to(interactive_map)  # use folium to add alternative tiles
        flm.LayerControl().add_to(interactive_map)  # use folium to add layer control
        
        html_file_path = 'out/elevation_points.html'
        created_file_path = self._save_interactive_plot(interactive_map, html_file_path)

        return svg_str, html_file_path
        pass
    
    def _plot_elevation_points(self, data, elevation_points):
        plt.ioff()


        from mpl_toolkits.axes_grid1 import make_axes_locatable

        superset = self.get_superset_df()
        elevation_points = elevation_points.to_crs(superset.crs)
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        
        superset.boundary.plot(ax=ax)

        divider = make_axes_locatable(ax)
        
        cax = divider.append_axes("right", size="5%", pad=0.1)        
        
        elevation_points.plot(ax=ax, 
                   column='elevation', 
                   legend=True, 
                   cax = cax,
                   legend_kwds={'label': "Elevation",
                         'orientation': "vertical"}
                   );
        
        ax.set_aspect('equal')
        fig.tight_layout()
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass


    def plot_usa_states(self, searchStrings = None, use_cache = False):
        '''
        data must be a dictionary if not None
        '''
        if searchStrings is None:
            usa_states = self.get_usa_states_df()
        else:
            usa_states = self.search_usa_states(searchStrings)
            
        if usa_states is None:
            return None, None
        
        svg_str = self._plot_usa_states(usa_states)

        flm = self._get_folium()

        superset = self.get_superset_df()
        
        interactive_map = superset.boundary.explore(
             scheme="naturalbreaks",  # use mapclassify's natural breaks scheme
             legend = False, 
             #k = 10, # use 10 bins
             name="base_boundary" # name of the layer in the map
        )

        marker_kwds = {}
        df_plot = self._plot_interactive_df(usa_states, 
                                            interactive_map, 
                                            categorical = True, 
                                            column = 'name', 
                                            marker_kwds = marker_kwds,
                                            )

        #flm.TileLayer('USA States', control=True).add_to(interactive_map)  # use folium to add alternative tiles
        flm.LayerControl().add_to(interactive_map)  # use folium to add layer control
        
        html_file_path = 'out/usa_states.html'
        created_file_path = self._save_interactive_plot(interactive_map, html_file_path)

        return svg_str, html_file_path
        pass
    
    def _plot_usa_states(self, usa_states):
        plt.ioff()


        superset = self.get_superset_df()
        usa_states = usa_states.to_crs(superset.crs)
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        
        superset.boundary.plot(ax=ax)
        
        usa_states.plot(ax=ax, 
                   column='name', 
                   legend=False, 
                   )
        
        usa_states.apply(lambda x: ax.annotate(
            text=x['name'], 
            xy=x.geometry.centroid.coords[0], 
            ha='center'), 
            axis=1)
        
        
        ax.set_aspect('equal')
        fig.tight_layout()
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass



    def plot_world_map_with_rivers_and_lakes(self, boundry = False, use_cache = False):
        plot_code, svg_str = self._get_cache_or_plot(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_world_map_with_rivers_and_lakes
                                )
        
        world = self.get_world_df()
        rivers_and_lakes = self.get_rivers_and_lakes_df()
        rivers_and_lakes = rivers_and_lakes.to_crs(world.crs)

        html_file_path = 'out/world_map_with_rivers_and_lakes.html'

        data = {
            'rivers_and_lakes': rivers_and_lakes
            }


        html_file_path = self._plot_interactive_search_result(data,
                                                              html_file_path = html_file_path)
        return plot_code, svg_str, html_file_path
        pass

    def _plot_world_map_with_rivers_and_lakes(self):
        plt.ioff()
        
        world = self.get_world_df()
        rivers_and_lakes = self.get_rivers_and_lakes_df()
        rivers_and_lakes = rivers_and_lakes.to_crs(world.crs)
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        
        world.plot(ax=ax, color='white', edgecolor='black')
        rivers_and_lakes.plot(ax=ax, marker='o', color='red', markersize = 10);

        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass


    def plot_world_gdp_per_capita(self, use_cache = False):
        plot_code, svg_str = self._get_cache_or_plot(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_world_gdp_per_capita
                                )
        return plot_code, svg_str
        pass


    def _plot_world_gdp_per_capita(self):
        plt.ioff()
        gpd = self._get_geopandas()


        from mpl_toolkits.axes_grid1 import make_axes_locatable
        
        world = self.get_world_df()
        
        world = world[(world.pop_est>0) & (world.name!="Antarctica")]
        
        if 'gdp_md_est' in list(world.columns):
            world['gdp_per_cap'] = world.gdp_md_est / world.pop_est
        else:
            world['gdp_per_cap'] = world.gdp_md / world.pop_est
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        
        divider = make_axes_locatable(ax)
        
        cax = divider.append_axes("right", size="5%", pad=0.1)        
        
        world.plot(ax=ax, 
                   column='gdp_per_cap', 
                   legend=True, 
                   cax = cax,
                   legend_kwds={'label': "GDP Per Capita by Country",
                         'orientation': "vertical"}
                   );
        
        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass


    def plot_world_population(self, use_cache = False):
        plot_code, svg_str = self._get_cache_or_plot(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_world_population
                                )
        return plot_code, svg_str
        pass

    def _plot_world_population(self):
        plt.ioff()
        gpd = self._get_geopandas()


        from mpl_toolkits.axes_grid1 import make_axes_locatable
        
        world = self.get_world_df()
        
        world = world[(world.pop_est>0) & (world.name!="Antarctica")]
        
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        
        #divider = make_axes_locatable(ax)
        #cax = divider.append_axes("right", size="5%", pad=0.1)        
        
        world.plot(ax=ax, 
                   column='pop_est', 
                   legend=True, 
                   #cax = cax,
                   legend_kwds={'label': "Population by Country",
                         'orientation': "horizontal"}
                   );
        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass

    def plot_world_no_population(self, use_cache = False):
        plot_code, svg_str = self._get_cache_or_plot(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_world_no_population
                                )
        return plot_code, svg_str
        pass

    def _plot_world_no_population(self):
        plt.ioff()
        gpd = self._get_geopandas()


        from mpl_toolkits.axes_grid1 import make_axes_locatable
        
        world = self.get_world_df()

        #create random missing population in data
        import numpy as np
        world.loc[np.random.choice(world.index, 40), 'pop_est'] = np.nan
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        
        missing_kwds = {
                 "color": "lightgrey",
                 "edgecolor": "red",
                 "hatch": "///",
                 "label": "Missing values",
                 }
        world.plot(
             ax=ax,
             column="pop_est",
             legend=True,
             scheme="quantiles",
             
             missing_kwds = missing_kwds,
         )
                
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass


    def plot_new_york(self, boundry = False, use_cache = False):
        plot_code, svg_str = self._get_cache_or_plot(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_new_york
                                )
        return plot_code, svg_str
        pass

    def _plot_new_york(self):
        plt.ioff()

        ny = self.get_ny_df()
        
        fig, ax = plt.subplots(1, 1, figsize = GeoPlotter._DEFAULT_FIGSIZE)
        
        ny.plot(ax=ax, 
                column="BoroName",
                cmap="Set1", 
                edgecolor='black',
                legend = True)
        
        ax.set_aspect('equal')
        fig.tight_layout()
        
        return SvgHelper.GET_SVG_STR(close_plot =True)
        pass

    def plot_interactive_new_york(self, use_cache = False):
        plot_code, html_file_path = self._get_cache_or_plot_interactive(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_interactive_new_york
                                )
        return plot_code, html_file_path
        pass

    def _plot_interactive_new_york(self):
        gpd = self._get_geopandas()

        ny = self.get_ny_df()

        interactive_map = ny.explore(
             column="BoroName", # make choropleth based on "BoroName" column
             tooltip="BoroName", # show "BoroName" value in tooltip (on hover)
             popup=True, # show all values in popup (on click)
             tiles="CartoDB positron", # use "CartoDB positron" tiles
             cmap="Set1", # use "Set1" matplotlib colormap
             style_kwds=dict(color="black") # use black outline
            )
        
        
        html_file_path = "out/new_york_interactive_map.html"
        created_file_path = self._save_interactive_plot(interactive_map, html_file_path)

        interactive_map = None
        
        if created_file_path:
            return html_file_path
        return None
        pass


    def plot_interactive_world_map(self, use_cache = False):
        plot_code, html_file_path = self._get_cache_or_plot_interactive(
                                use_cache = use_cache, 
                                calling_method = inspect.stack()[0][3],
                                method_to_call = self._plot_interactive_world_map
                                )
        return plot_code, html_file_path
        pass

    def _plot_interactive_world_map(self):
        flm = self._get_folium()
   
        world = self.get_world_df()
        cities = self.get_cities_df()
        cities = cities.to_crs(world.crs)
        
        interactive_map = world.explore(
             column="pop_est",  # make choropleth based on "BoroName" column
             scheme="naturalbreaks",  # use mapclassify's natural breaks scheme
             legend=True, # show legend
             k=10, # use 10 bins
             legend_kwds=dict(colorbar=False), # do not use colorbar
             name="countries" # name of the layer in the map
        )
        
        cities.explore(
             m=interactive_map, # pass the map object
             color="red", # use red color on all points
             marker_kwds=dict(radius=2, fill=True), # make marker radius 2px with fill
             tooltip="name", # show "name" column in the tooltip
             tooltip_kwds=dict(labels=False), # do not show column label in the tooltip
             name="cities" # name of the layer in the map
        )
        
        #flm.TileLayer('World and Cities', control=True).add_to(interactive_map)  # use folium to add alternative tiles
        flm.LayerControl().add_to(interactive_map)  # use folium to add layer control
        
        html_file_path = "out/world_and_cities_interactive_map.html"
        created_file_path = self._save_interactive_plot(interactive_map, html_file_path)


        interactive_map = None
        
        if created_file_path:
            return html_file_path
        return None
        pass

    def _save_interactive_plot(self, interactive_map, html_file_path):
        html_file_path = os.path.abspath(html_file_path)
        outputFolder = os.path.dirname(html_file_path)
        Path(outputFolder).mkdir(parents=True, exist_ok=True)
        
        interactive_map.save(html_file_path)
        return f"{html_file_path}"

    def _get_cache_or_plot_interactive(self,
                            use_cache = False, 
                            calling_method = None,
                            method_to_call = None,
                            **kwargs
                           ):
        try:
            verbose = self.verbose
            self.cacheUtil = CacheUtil(use_cache = use_cache, verbose = verbose)
            
            cache_file_path, exists_cached_file = self.cacheUtil.get_cached_path(
                    self.__class__, calling_method,
                    None,
                    **kwargs,
                    file_prefix = f"{self.__class__.__name__}_{calling_method}_",
                    )
            
            if use_cache and exists_cached_file:
                plot_code, html_file_path = self.cacheUtil.load_file(cache_file_path)
                if os.path.isfile(html_file_path):
                    return plot_code, html_file_path
            
            html_file_path = method_to_call()
    
            #close plot
            plt.clf()
            plt.close()
    
            function_source_code = get_function_source_code(method_to_call, 
                                                            strip_tab = False)
            
            function_source_code = strip_source_code_tabs(function_source_code[3:-5])
            function_source_code = "\n".join(function_source_code)
            
            plot_code = f"""
import folium as flm
import geopandas as gpd


{function_source_code}
    
            """
            
            if not html_file_path:
                if self.verbose:
                    print(f"Error: Could not create html file")
            else:
                self.cacheUtil.save_file((plot_code, html_file_path), cache_file_path)
            
            return plot_code, html_file_path
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:{e}
            failed to get cache file or interactive plotting of df
            """
            self._log(sError)
            self._log(traceback.format_exc())
    
        return None, None
        pass

    def _get_cache_or_plot(self,
                            use_cache = False, 
                            calling_method = None,
                            method_to_call = None,
                            close_plot = True,
                            **kwargs
                           ):
        try:
            verbose = self.verbose
            self.cacheUtil = CacheUtil(use_cache = use_cache, verbose = verbose)
            
            cache_file_path, exists_cached_file = self.cacheUtil.get_cached_path(
                    self.__class__, calling_method,
                    None,
                    **kwargs,
                    file_prefix = f"{self.__class__.__name__}_{calling_method}_",
                    )
            
            if use_cache and exists_cached_file:
                return self.cacheUtil.load_file(cache_file_path)
            
            # changed by b thakur Sep 22, 2022
            #svg_str = method_to_call()
            
            from xv.util import run_with_timeout
            timeout = 5 #sec
            svg_str = run_with_timeout(f'Thread_{self.__class__.__name__}.{inspect.stack()[0][3]}', 
                                      method_to_call, 
                                      timeout = timeout,
                                      verbose = self.verbose)
            
            if not svg_str:
                sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:
                failed to run method = {method_to_call.__name__}
                """
                self._log(sError)
                return None, None
                
            
            if close_plot:
                plt.clf()
                plt.close()
    
    
            function_source_code = get_function_source_code(method_to_call, 
                                                            strip_tab = False)
            
            function_source_code = strip_source_code_tabs(function_source_code[3:-5])
            function_source_code = "\n".join(function_source_code)
            
            plot_code = f"""
    import matplotlib.pyplot as plt
    import geopandas as gpd
    
    
    {function_source_code}
    
    plt.show()
            """
            
            if not svg_str:
                if self.verbose:
                    print(f"Error: svg_str could not be initialized")
            else:
                self.cacheUtil.save_file((plot_code, svg_str), cache_file_path)
            
            return plot_code, svg_str
        except Exception as e:
            sError = f"""Error {self.__class__.__name__}.{inspect.stack()[0][3]}:{e}
            failed to get cache file or plotting of df
            """
            self._log(sError)
            self._log(traceback.format_exc())
    
        return None, None
        pass
    
    
    pass
#class GeoPlotter:
