Skip to content

folium_mapping module

This module provides a custom Map class that extends folium.Map

Map (Map)

A custom Map class that extends folium.Map.

Source code in biospat/foliummap.py
class Map(folium.Map):
    """A custom Map class that extends folium.Map."""

    def __init__(self, center=(0, 0), zoom=2, tiles="OpenStreetMap", **kwargs):
        """Initializes the Map object.

        Args:
            center (tuple, optional): The initial center of the map as (latitude, longitude). Defaults to (0, 0).
            zoom (int, optional): The initial zoom level of the map. Defaults to 2.
            tiles (str, optional): The tile layer to use for the map. Defaults to "OpenStreetMap".
                Available options:
                    - "OpenStreetMap": Standard street map.
                    - "Esri.WorldImagery": Satellite imagery from Esri.
                    - "Esri.WorldTerrain": Terrain map from Esri.
                    - "Esri.WorldStreetMap": Street map from Esri.
                    - "CartoDB.Positron": A light and minimalist map style.
                    - "CartoDB.DarkMatter": A dark-themed map style.

            **kwargs: Additional keyword arguments for the folium.Map class.
        """
        super().__init__(location=center, zoom_start=zoom, tiles=tiles, **kwargs)

    def add_basemap(self, basemap):
        """Add a basemap to the map using folium's TileLayer.

        Args:
            basemap (str): The name of the basemap to add.
        """
        # Folium built-in tile layers
        builtin_tiles = [
            "OpenStreetMap",
            "OpenTopoMap",
            "Esri.WorldImagery",
            "Esri.WorldTerrain",
            "CartoDB Positron",
            "CartoDB Dark_Matter",
        ]

        if basemap in builtin_tiles:
            folium.TileLayer(basemap, name=basemap).add_to(self)

        else:
            custom_tiles = {
                "OpenTopoMap": "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
                "Esri.WorldImagery": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
            }

            if basemap in custom_tiles:
                folium.TileLayer(
                    tiles=custom_tiles[basemap], attr="Custom Attribution", name=basemap
                ).add_to(self)
            else:
                raise ValueError(f"Basemap '{basemap}' is not available.")

    def add_geojson(
        self,
        data,
        zoom_to_layer=True,
        hover_style=None,
        **kwargs,
    ):
        """Adds a GeoJSON layer to the map.

        Args:
            data (str or dict): The GeoJSON data. Can be a file path (str) or a dictionary.
            zoom_to_layer (bool, optional): Whether to zoom to the layer's bounds. Defaults to True.
            hover_style (dict, optional): Style to apply when hovering over features. Defaults to {"color": "yellow", "fillOpacity": 0.2}.
            **kwargs: Additional keyword arguments for the folium.GeoJson layer.

        Raises:
            ValueError: If the data type is invalid.
        """
        import geopandas as gpd

        if hover_style is None:
            hover_style = {"color": "yellow", "fillOpacity": 0.2}

        if isinstance(data, str):
            gdf = gpd.read_file(data)
            geojson = gdf.__geo_interface__
        elif isinstance(data, dict):
            geojson = data

        geojson = folium.GeoJson(data=geojson, **kwargs)
        geojson.add_to(self)

    def add_shp(self, data, **kwargs):
        """Adds a shapefile to the map.

        Args:
            data (str): The file path to the shapefile.
            **kwargs: Additional keyword arguments for the GeoJSON layer.
        """
        import geopandas as gpd

        gdf = gpd.read_file(data)
        gdf = gdf.to_crs(epsg=4326)
        geojson = gdf.__geo_interface__
        self.add_geojson(geojson, **kwargs)

    def add_shp_from_url(self, url, **kwargs):
        """Adds a shapefile from a URL to the map using Folium.

        This function downloads the shapefile components (.shp, .shx, .dbf) from the specified URL, stores them
        in a temporary directory, reads the shapefile using Geopandas, converts it to GeoJSON format, and
        then adds it to the Folium map. If the shapefile's coordinate reference system (CRS) is not set, it assumes
        the CRS to be EPSG:4326 (WGS84).

        Args:
            url (str): The URL pointing to the shapefile's location. The URL should be a raw GitHub link to
                        the shapefile components (e.g., ".shp", ".shx", ".dbf").
            **kwargs: Additional keyword arguments to pass to the `GeoJson` method for styling and
                        configuring the GeoJSON layer on the Folium map.
        """
        try:
            base_url = url.replace("github.com", "raw.githubusercontent.com").replace(
                "blob/", ""
            )
            shp_url = base_url + ".shp"
            shx_url = base_url + ".shx"
            dbf_url = base_url + ".dbf"

            temp_dir = tempfile.mkdtemp()

            shp_file = requests.get(shp_url).content
            shx_file = requests.get(shx_url).content
            dbf_file = requests.get(dbf_url).content

            with open(os.path.join(temp_dir, "data.shp"), "wb") as f:
                f.write(shp_file)
            with open(os.path.join(temp_dir, "data.shx"), "wb") as f:
                f.write(shx_file)
            with open(os.path.join(temp_dir, "data.dbf"), "wb") as f:
                f.write(dbf_file)

            gdf = gpd.read_file(os.path.join(temp_dir, "data.shp"))

            if gdf.crs is None:
                gdf.set_crs("EPSG:4326", allow_override=True, inplace=True)

            geojson = gdf.__geo_interface__

            folium.GeoJson(geojson, **kwargs).add_to(self)

            shutil.rmtree(temp_dir)

        except Exception as e:
            print(f"Error loading shapefile: {e}")

    def add_gdf(self, gdf, **kwargs):
        """Adds a GeoDataFrame to the map.

        Args:
            gdf (geopandas.GeoDataFrame): The GeoDataFrame to add.
            **kwargs: Additional keyword arguments for the GeoJSON layer.
        """
        gdf = gdf.to_crs(epsg=4326)
        geojson = gdf.__geo_interface__
        self.add_geojson(geojson, **kwargs)

    def add_vector(self, data, **kwargs):
        """Adds vector data to the map.

        Args:
            data (str, geopandas.GeoDataFrame, or dict): The vector data. Can be a file path, GeoDataFrame, or GeoJSON dictionary.
            **kwargs: Additional keyword arguments for the GeoJSON layer.

        Raises:
            ValueError: If the data type is invalid.
        """
        import geopandas as gpd

        if isinstance(data, str):
            gdf = gpd.read_file(data)
            self.add_gdf(gdf, **kwargs)
        elif isinstance(data, gpd.GeoDataFrame):
            self.add_gdf(data, **kwargs)
        elif isinstance(data, dict):
            self.add_geojson(data, **kwargs)
        else:
            raise ValueError("Invalid data type")

    def add_layer_control(self):
        """Adds a layer control widget to the map."""
        folium.LayerControl().add_to(self)

    def add_split_map(
        self,
        left,
        right="cartodbpositron",
        name_left="Left Raster",
        name_right="Right Raster",
        colormap_left=None,
        colormap_right=None,
        opacity_left=1.0,
        opacity_right=1.0,
        **kwargs,
    ):
        """
        Adds a split map with one or both sides displaying a raster GeoTIFF, with independent colormaps.

        Args:
            left (str or TileClient): Left map layer (Tile URL, basemap name, or GeoTIFF path).
            right (str or TileClient): Right map layer (Tile URL, basemap name, or GeoTIFF path).
            name_left (str, optional): Name for the left raster layer. Defaults to "Left Raster".
            name_right (str, optional): Name for the right raster layer. Defaults to "Right Raster".
            colormap_left (str, optional): Colormap for the left raster. Defaults to None.
            colormap_right (str, optional): Colormap for the right raster. Defaults to None.
            opacity_left (float, optional): Opacity of the left raster. Defaults to 1.0.
            opacity_right (float, optional): Opacity of the right raster. Defaults to 1.0.
            **kwargs: Additional arguments for the tile layers.

        Returns:
            None
        """

        # Convert left layer if it's a raster file/URL
        if isinstance(left, str) and left.endswith(".tif"):
            client_left = TileClient(left)
            left_layer = get_folium_tile_layer(
                client_left,
                name=name_left,
                colormap=colormap_left,
                opacity=opacity_left,
                **kwargs,
            )
        else:
            left_layer = folium.TileLayer(left, overlay=True, **kwargs)

        # Convert right layer if it's a raster file/URL
        if isinstance(right, str) and right.endswith(".tif"):
            client_right = TileClient(right)
            right_layer = get_folium_tile_layer(
                client_right,
                name=name_right,
                colormap=colormap_right,
                opacity=opacity_right,
                **kwargs,
            )
        else:
            right_layer = folium.TileLayer(right, overlay=True, **kwargs)

        # Add layers to the map
        left_layer.add_to(self)
        right_layer.add_to(self)

        # Create split-screen effect
        split_map = folium.plugins.SideBySideLayers(left_layer, right_layer)
        split_map.add_to(self)

__init__(self, center=(0, 0), zoom=2, tiles='OpenStreetMap', **kwargs) special

Initializes the Map object.

Parameters:

Name Type Description Default
center tuple

The initial center of the map as (latitude, longitude). Defaults to (0, 0).

(0, 0)
zoom int

The initial zoom level of the map. Defaults to 2.

2
tiles str

The tile layer to use for the map. Defaults to "OpenStreetMap". Available options: - "OpenStreetMap": Standard street map. - "Esri.WorldImagery": Satellite imagery from Esri. - "Esri.WorldTerrain": Terrain map from Esri. - "Esri.WorldStreetMap": Street map from Esri. - "CartoDB.Positron": A light and minimalist map style. - "CartoDB.DarkMatter": A dark-themed map style.

'OpenStreetMap'
**kwargs

Additional keyword arguments for the folium.Map class.

{}
Source code in biospat/foliummap.py
def __init__(self, center=(0, 0), zoom=2, tiles="OpenStreetMap", **kwargs):
    """Initializes the Map object.

    Args:
        center (tuple, optional): The initial center of the map as (latitude, longitude). Defaults to (0, 0).
        zoom (int, optional): The initial zoom level of the map. Defaults to 2.
        tiles (str, optional): The tile layer to use for the map. Defaults to "OpenStreetMap".
            Available options:
                - "OpenStreetMap": Standard street map.
                - "Esri.WorldImagery": Satellite imagery from Esri.
                - "Esri.WorldTerrain": Terrain map from Esri.
                - "Esri.WorldStreetMap": Street map from Esri.
                - "CartoDB.Positron": A light and minimalist map style.
                - "CartoDB.DarkMatter": A dark-themed map style.

        **kwargs: Additional keyword arguments for the folium.Map class.
    """
    super().__init__(location=center, zoom_start=zoom, tiles=tiles, **kwargs)

add_basemap(self, basemap)

Add a basemap to the map using folium's TileLayer.

Parameters:

Name Type Description Default
basemap str

The name of the basemap to add.

required
Source code in biospat/foliummap.py
def add_basemap(self, basemap):
    """Add a basemap to the map using folium's TileLayer.

    Args:
        basemap (str): The name of the basemap to add.
    """
    # Folium built-in tile layers
    builtin_tiles = [
        "OpenStreetMap",
        "OpenTopoMap",
        "Esri.WorldImagery",
        "Esri.WorldTerrain",
        "CartoDB Positron",
        "CartoDB Dark_Matter",
    ]

    if basemap in builtin_tiles:
        folium.TileLayer(basemap, name=basemap).add_to(self)

    else:
        custom_tiles = {
            "OpenTopoMap": "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
            "Esri.WorldImagery": "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
        }

        if basemap in custom_tiles:
            folium.TileLayer(
                tiles=custom_tiles[basemap], attr="Custom Attribution", name=basemap
            ).add_to(self)
        else:
            raise ValueError(f"Basemap '{basemap}' is not available.")

add_gdf(self, gdf, **kwargs)

Adds a GeoDataFrame to the map.

Parameters:

Name Type Description Default
gdf geopandas.GeoDataFrame

The GeoDataFrame to add.

required
**kwargs

Additional keyword arguments for the GeoJSON layer.

{}
Source code in biospat/foliummap.py
def add_gdf(self, gdf, **kwargs):
    """Adds a GeoDataFrame to the map.

    Args:
        gdf (geopandas.GeoDataFrame): The GeoDataFrame to add.
        **kwargs: Additional keyword arguments for the GeoJSON layer.
    """
    gdf = gdf.to_crs(epsg=4326)
    geojson = gdf.__geo_interface__
    self.add_geojson(geojson, **kwargs)

add_geojson(self, data, zoom_to_layer=True, hover_style=None, **kwargs)

Adds a GeoJSON layer to the map.

Parameters:

Name Type Description Default
data str or dict

The GeoJSON data. Can be a file path (str) or a dictionary.

required
zoom_to_layer bool

Whether to zoom to the layer's bounds. Defaults to True.

True
hover_style dict

Style to apply when hovering over features. Defaults to {"color": "yellow", "fillOpacity": 0.2}.

None
**kwargs

Additional keyword arguments for the folium.GeoJson layer.

{}

Exceptions:

Type Description
ValueError

If the data type is invalid.

Source code in biospat/foliummap.py
def add_geojson(
    self,
    data,
    zoom_to_layer=True,
    hover_style=None,
    **kwargs,
):
    """Adds a GeoJSON layer to the map.

    Args:
        data (str or dict): The GeoJSON data. Can be a file path (str) or a dictionary.
        zoom_to_layer (bool, optional): Whether to zoom to the layer's bounds. Defaults to True.
        hover_style (dict, optional): Style to apply when hovering over features. Defaults to {"color": "yellow", "fillOpacity": 0.2}.
        **kwargs: Additional keyword arguments for the folium.GeoJson layer.

    Raises:
        ValueError: If the data type is invalid.
    """
    import geopandas as gpd

    if hover_style is None:
        hover_style = {"color": "yellow", "fillOpacity": 0.2}

    if isinstance(data, str):
        gdf = gpd.read_file(data)
        geojson = gdf.__geo_interface__
    elif isinstance(data, dict):
        geojson = data

    geojson = folium.GeoJson(data=geojson, **kwargs)
    geojson.add_to(self)

add_layer_control(self)

Adds a layer control widget to the map.

Source code in biospat/foliummap.py
def add_layer_control(self):
    """Adds a layer control widget to the map."""
    folium.LayerControl().add_to(self)

add_shp(self, data, **kwargs)

Adds a shapefile to the map.

Parameters:

Name Type Description Default
data str

The file path to the shapefile.

required
**kwargs

Additional keyword arguments for the GeoJSON layer.

{}
Source code in biospat/foliummap.py
def add_shp(self, data, **kwargs):
    """Adds a shapefile to the map.

    Args:
        data (str): The file path to the shapefile.
        **kwargs: Additional keyword arguments for the GeoJSON layer.
    """
    import geopandas as gpd

    gdf = gpd.read_file(data)
    gdf = gdf.to_crs(epsg=4326)
    geojson = gdf.__geo_interface__
    self.add_geojson(geojson, **kwargs)

add_shp_from_url(self, url, **kwargs)

Adds a shapefile from a URL to the map using Folium.

This function downloads the shapefile components (.shp, .shx, .dbf) from the specified URL, stores them in a temporary directory, reads the shapefile using Geopandas, converts it to GeoJSON format, and then adds it to the Folium map. If the shapefile's coordinate reference system (CRS) is not set, it assumes the CRS to be EPSG:4326 (WGS84).

Parameters:

Name Type Description Default
url str

The URL pointing to the shapefile's location. The URL should be a raw GitHub link to the shapefile components (e.g., ".shp", ".shx", ".dbf").

required
**kwargs

Additional keyword arguments to pass to the GeoJson method for styling and configuring the GeoJSON layer on the Folium map.

{}
Source code in biospat/foliummap.py
def add_shp_from_url(self, url, **kwargs):
    """Adds a shapefile from a URL to the map using Folium.

    This function downloads the shapefile components (.shp, .shx, .dbf) from the specified URL, stores them
    in a temporary directory, reads the shapefile using Geopandas, converts it to GeoJSON format, and
    then adds it to the Folium map. If the shapefile's coordinate reference system (CRS) is not set, it assumes
    the CRS to be EPSG:4326 (WGS84).

    Args:
        url (str): The URL pointing to the shapefile's location. The URL should be a raw GitHub link to
                    the shapefile components (e.g., ".shp", ".shx", ".dbf").
        **kwargs: Additional keyword arguments to pass to the `GeoJson` method for styling and
                    configuring the GeoJSON layer on the Folium map.
    """
    try:
        base_url = url.replace("github.com", "raw.githubusercontent.com").replace(
            "blob/", ""
        )
        shp_url = base_url + ".shp"
        shx_url = base_url + ".shx"
        dbf_url = base_url + ".dbf"

        temp_dir = tempfile.mkdtemp()

        shp_file = requests.get(shp_url).content
        shx_file = requests.get(shx_url).content
        dbf_file = requests.get(dbf_url).content

        with open(os.path.join(temp_dir, "data.shp"), "wb") as f:
            f.write(shp_file)
        with open(os.path.join(temp_dir, "data.shx"), "wb") as f:
            f.write(shx_file)
        with open(os.path.join(temp_dir, "data.dbf"), "wb") as f:
            f.write(dbf_file)

        gdf = gpd.read_file(os.path.join(temp_dir, "data.shp"))

        if gdf.crs is None:
            gdf.set_crs("EPSG:4326", allow_override=True, inplace=True)

        geojson = gdf.__geo_interface__

        folium.GeoJson(geojson, **kwargs).add_to(self)

        shutil.rmtree(temp_dir)

    except Exception as e:
        print(f"Error loading shapefile: {e}")

add_split_map(self, left, right='cartodbpositron', name_left='Left Raster', name_right='Right Raster', colormap_left=None, colormap_right=None, opacity_left=1.0, opacity_right=1.0, **kwargs)

Adds a split map with one or both sides displaying a raster GeoTIFF, with independent colormaps.

Parameters:

Name Type Description Default
left str or TileClient

Left map layer (Tile URL, basemap name, or GeoTIFF path).

required
right str or TileClient

Right map layer (Tile URL, basemap name, or GeoTIFF path).

'cartodbpositron'
name_left str

Name for the left raster layer. Defaults to "Left Raster".

'Left Raster'
name_right str

Name for the right raster layer. Defaults to "Right Raster".

'Right Raster'
colormap_left str

Colormap for the left raster. Defaults to None.

None
colormap_right str

Colormap for the right raster. Defaults to None.

None
opacity_left float

Opacity of the left raster. Defaults to 1.0.

1.0
opacity_right float

Opacity of the right raster. Defaults to 1.0.

1.0
**kwargs

Additional arguments for the tile layers.

{}

Returns:

Type Description

None

Source code in biospat/foliummap.py
def add_split_map(
    self,
    left,
    right="cartodbpositron",
    name_left="Left Raster",
    name_right="Right Raster",
    colormap_left=None,
    colormap_right=None,
    opacity_left=1.0,
    opacity_right=1.0,
    **kwargs,
):
    """
    Adds a split map with one or both sides displaying a raster GeoTIFF, with independent colormaps.

    Args:
        left (str or TileClient): Left map layer (Tile URL, basemap name, or GeoTIFF path).
        right (str or TileClient): Right map layer (Tile URL, basemap name, or GeoTIFF path).
        name_left (str, optional): Name for the left raster layer. Defaults to "Left Raster".
        name_right (str, optional): Name for the right raster layer. Defaults to "Right Raster".
        colormap_left (str, optional): Colormap for the left raster. Defaults to None.
        colormap_right (str, optional): Colormap for the right raster. Defaults to None.
        opacity_left (float, optional): Opacity of the left raster. Defaults to 1.0.
        opacity_right (float, optional): Opacity of the right raster. Defaults to 1.0.
        **kwargs: Additional arguments for the tile layers.

    Returns:
        None
    """

    # Convert left layer if it's a raster file/URL
    if isinstance(left, str) and left.endswith(".tif"):
        client_left = TileClient(left)
        left_layer = get_folium_tile_layer(
            client_left,
            name=name_left,
            colormap=colormap_left,
            opacity=opacity_left,
            **kwargs,
        )
    else:
        left_layer = folium.TileLayer(left, overlay=True, **kwargs)

    # Convert right layer if it's a raster file/URL
    if isinstance(right, str) and right.endswith(".tif"):
        client_right = TileClient(right)
        right_layer = get_folium_tile_layer(
            client_right,
            name=name_right,
            colormap=colormap_right,
            opacity=opacity_right,
            **kwargs,
        )
    else:
        right_layer = folium.TileLayer(right, overlay=True, **kwargs)

    # Add layers to the map
    left_layer.add_to(self)
    right_layer.add_to(self)

    # Create split-screen effect
    split_map = folium.plugins.SideBySideLayers(left_layer, right_layer)
    split_map.add_to(self)

add_vector(self, data, **kwargs)

Adds vector data to the map.

Parameters:

Name Type Description Default
data str, geopandas.GeoDataFrame, or dict

The vector data. Can be a file path, GeoDataFrame, or GeoJSON dictionary.

required
**kwargs

Additional keyword arguments for the GeoJSON layer.

{}

Exceptions:

Type Description
ValueError

If the data type is invalid.

Source code in biospat/foliummap.py
def add_vector(self, data, **kwargs):
    """Adds vector data to the map.

    Args:
        data (str, geopandas.GeoDataFrame, or dict): The vector data. Can be a file path, GeoDataFrame, or GeoJSON dictionary.
        **kwargs: Additional keyword arguments for the GeoJSON layer.

    Raises:
        ValueError: If the data type is invalid.
    """
    import geopandas as gpd

    if isinstance(data, str):
        gdf = gpd.read_file(data)
        self.add_gdf(gdf, **kwargs)
    elif isinstance(data, gpd.GeoDataFrame):
        self.add_gdf(data, **kwargs)
    elif isinstance(data, dict):
        self.add_geojson(data, **kwargs)
    else:
        raise ValueError("Invalid data type")