Skip to content

foliumap module

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

Map (Map)

Custom Map class to handle portgeos.

Source code in portgeo/foliumap.py
class Map(folium.Map):
    """Custom Map class to handle portgeos."""

    def __init__(self, center=[0, 0], zoom=2, **kwargs):
        super().__init__(location=center, zoom_start=zoom, **kwargs)

    def add_geojson(
        self,
        data,
        tooltip_fields=None,
        tooltip_aliases=None,
        highlight=True,
        style_function=None,
        **kwargs,
    ):
        """Add a GeoJSON layer to the map with optional tooltip and highlight on hover.

        Args:
            data (str or dict): Path to the GeoJSON file or GeoJSON data.
            tooltip_fields (list): Fields to include in the tooltip.
            tooltip_aliases (list): Optional aliases for tooltip fields.
            highlight (bool): Whether to highlight geometry on hover.
            style_function (function): Optional base style function.
            **kwargs: Additional arguments for folium.GeoJson.

        Raises:
            TypeError: If data is not a string or dictionary.
            ValueError: If data is not a valid GeoJSON.
        """
        import geopandas as gpd
        import folium

        if isinstance(data, str):
            gdf = gpd.read_file(data)
            geojson = gdf.__geo_interface__
        elif isinstance(data, dict):
            geojson = data
        else:
            raise TypeError("Data must be a file path or GeoJSON dictionary.")

        # Default style (optional)
        default_style = style_function or (
            lambda f: {"fillOpacity": 0.5, "weight": 1, "color": "gray"}
        )

        # Optional highlight effect on hover
        highlight_fn = (
            (lambda f: {"weight": 3, "color": "yellow", "fillOpacity": 0.2})
            if highlight
            else None
        )

        # Tooltip configuration
        tooltip = (
            folium.GeoJsonTooltip(
                fields=tooltip_fields,
                aliases=tooltip_aliases or tooltip_fields,
                localize=True,
                sticky=True,
                labels=True,
            )
            if tooltip_fields
            else None
        )

        folium.GeoJson(
            data=geojson,
            style_function=default_style,
            highlight_function=highlight_fn,
            tooltip=tooltip,
            **kwargs,
        ).add_to(self)

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

        Args:
            data (str): Path to the shapefile.
            **kwargs: Additional arguments for folium.GeoJson.
        """

        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_gdf(self, gdf, **kwargs):
        """Add a GeoDataFrame layer to the map.

        Args:
            gdf (GeoDataFrame): GeoDataFrame to add.
            **kwargs: Additional arguments for folium.GeoJson.
        """

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

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

        Args:
            data (str or GeoDataFrame or dict): Path to the vector file,
                GeoDataFrame, or GeoJSON data.
            **kwargs: Additional arguments for folium.GeoJson.
        """
        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(
                "Unsupported data type. Please provide a GeoDataFrame, GeoJSON, or file path."
            )

    def add_layer_control(self):
        """Add a layer control to the map.

        Args:
            **kwargs: Additional arguments for folium.LayerControl.
        """
        folium.LayerControl().add_to(self)
        folium.plugins.Fullscreen().add_to(self)

    def add_split_map(self, left="openstreetmap", right="cartodbpositron", **kwargs):
        """Add a split map to the map.
        Args:
            left (str): Name of the left layer. Default is "openstreetmap".
            right (str): Name of the right layer. Default is "cartodbpositron".
            **kwargs: Additional arguments for folium.TileLayer.
        """
        from localtileserver import get_folium_tile_layer
        import os

        if left.startswith("http") or os.path.exists(left):
            layer_left = get_folium_tile_layer(left, overlay=True, **kwargs)
        else:
            layer_left = folium.TileLayer(left, overlay=True, **kwargs)

        if right.startswith("http") or os.path.exists(right):
            layer_right = get_folium_tile_layer(right, overlay=True, **kwargs)
        else:
            layer_right = folium.TileLayer(right, overlay=True, **kwargs)

        sbs = folium.plugins.SideBySideLayers(
            layer_left=layer_left, layer_right=layer_right
        )

        layer_left.add_to(self)
        layer_right.add_to(self)
        sbs.add_to(self)

    def add_choropleth(
        self,
        gdf,
        column,
        join_col,
        key_on="feature.properties.name",
        fill_color="YlGn",
        legend_name=None,
        tooltip_fields=None,
        tooltip_aliases=None,
        **kwargs,
    ):
        """Add a Choropleth layer to the map.

        Args:
            gdf (GeoDataFrame or str): GeoDataFrame or path to file with geometries and data.
            column (str): Column in the GeoDataFrame to color by.
            join_col (str): Column to join GeoJSON and data on.
            key_on (str): GeoJSON property key to match join_col (e.g., 'feature.properties.<name>').
            fill_color (str): Color scheme for the choropleth.
            legend_name (str): Name for the legend.
            **kwargs: Additional arguments for folium.Choropleth.
        """
        import geopandas as gpd
        import json

        # Case 1: file path
        if isinstance(gdf, str):
            gdf = gpd.read_file(gdf)
            gdf = gdf.to_crs(epsg=4326)
            geojson = json.loads(gdf.to_json())

        # Case 2: GeoJSON dictionary
        elif isinstance(gdf, dict):
            if "features" not in gdf:
                raise ValueError("GeoJSON dict must have a 'features' key.")
            try:
                gdf = gpd.GeoDataFrame.from_features(gdf["features"])
                gdf = gdf.set_geometry("geometry").set_crs(epsg=4326)
                geojson = gdf.__geo_interface__  # or json.loads(gdf.to_json())
            except Exception as e:
                raise ValueError(f"Failed to parse GeoJSON dictionary: {e}")

        # Case 3: GeoDataFrame
        elif isinstance(gdf, gpd.GeoDataFrame):
            gdf = gdf.to_crs(epsg=4326)
            geojson = json.loads(gdf.to_json())

        else:
            raise TypeError(
                "gdf must be a GeoDataFrame, file path, or GeoJSON dictionary."
            )

        if join_col not in gdf.columns or column not in gdf.columns:
            raise ValueError(
                f"'{join_col}' and/or '{column}' not found in GeoDataFrame columns: {list(gdf.columns)}"
            )

        folium.Choropleth(
            geo_data=geojson,
            data=gdf[[join_col, column]],
            columns=[join_col, column],
            key_on=key_on,
            fill_color=fill_color,
            fill_opacity=0.7,
            line_opacity=0.2,
            legend_name=legend_name or column,
            **kwargs,
        ).add_to(self)

        # Add tooltips on hover (optional)
        if tooltip_fields:
            folium.GeoJson(
                geojson,
                tooltip=folium.GeoJsonTooltip(
                    fields=tooltip_fields,
                    aliases=tooltip_aliases or tooltip_fields,
                    localize=True,
                    sticky=True,
                    labels=True,
                ),
                style_function=lambda x: {
                    "fillOpacity": 0,
                    "weight": 0,
                },  # transparent overlay
            ).add_to(self)

add_choropleth(self, gdf, column, join_col, key_on='feature.properties.name', fill_color='YlGn', legend_name=None, tooltip_fields=None, tooltip_aliases=None, **kwargs)

Add a Choropleth layer to the map.

Parameters:

Name Type Description Default
gdf GeoDataFrame or str

GeoDataFrame or path to file with geometries and data.

required
column str

Column in the GeoDataFrame to color by.

required
join_col str

Column to join GeoJSON and data on.

required
key_on str

GeoJSON property key to match join_col (e.g., 'feature.properties.').

'feature.properties.name'
fill_color str

Color scheme for the choropleth.

'YlGn'
legend_name str

Name for the legend.

None
**kwargs

Additional arguments for folium.Choropleth.

{}
Source code in portgeo/foliumap.py
def add_choropleth(
    self,
    gdf,
    column,
    join_col,
    key_on="feature.properties.name",
    fill_color="YlGn",
    legend_name=None,
    tooltip_fields=None,
    tooltip_aliases=None,
    **kwargs,
):
    """Add a Choropleth layer to the map.

    Args:
        gdf (GeoDataFrame or str): GeoDataFrame or path to file with geometries and data.
        column (str): Column in the GeoDataFrame to color by.
        join_col (str): Column to join GeoJSON and data on.
        key_on (str): GeoJSON property key to match join_col (e.g., 'feature.properties.<name>').
        fill_color (str): Color scheme for the choropleth.
        legend_name (str): Name for the legend.
        **kwargs: Additional arguments for folium.Choropleth.
    """
    import geopandas as gpd
    import json

    # Case 1: file path
    if isinstance(gdf, str):
        gdf = gpd.read_file(gdf)
        gdf = gdf.to_crs(epsg=4326)
        geojson = json.loads(gdf.to_json())

    # Case 2: GeoJSON dictionary
    elif isinstance(gdf, dict):
        if "features" not in gdf:
            raise ValueError("GeoJSON dict must have a 'features' key.")
        try:
            gdf = gpd.GeoDataFrame.from_features(gdf["features"])
            gdf = gdf.set_geometry("geometry").set_crs(epsg=4326)
            geojson = gdf.__geo_interface__  # or json.loads(gdf.to_json())
        except Exception as e:
            raise ValueError(f"Failed to parse GeoJSON dictionary: {e}")

    # Case 3: GeoDataFrame
    elif isinstance(gdf, gpd.GeoDataFrame):
        gdf = gdf.to_crs(epsg=4326)
        geojson = json.loads(gdf.to_json())

    else:
        raise TypeError(
            "gdf must be a GeoDataFrame, file path, or GeoJSON dictionary."
        )

    if join_col not in gdf.columns or column not in gdf.columns:
        raise ValueError(
            f"'{join_col}' and/or '{column}' not found in GeoDataFrame columns: {list(gdf.columns)}"
        )

    folium.Choropleth(
        geo_data=geojson,
        data=gdf[[join_col, column]],
        columns=[join_col, column],
        key_on=key_on,
        fill_color=fill_color,
        fill_opacity=0.7,
        line_opacity=0.2,
        legend_name=legend_name or column,
        **kwargs,
    ).add_to(self)

    # Add tooltips on hover (optional)
    if tooltip_fields:
        folium.GeoJson(
            geojson,
            tooltip=folium.GeoJsonTooltip(
                fields=tooltip_fields,
                aliases=tooltip_aliases or tooltip_fields,
                localize=True,
                sticky=True,
                labels=True,
            ),
            style_function=lambda x: {
                "fillOpacity": 0,
                "weight": 0,
            },  # transparent overlay
        ).add_to(self)

add_gdf(self, gdf, **kwargs)

Add a GeoDataFrame layer to the map.

Parameters:

Name Type Description Default
gdf GeoDataFrame

GeoDataFrame to add.

required
**kwargs

Additional arguments for folium.GeoJson.

{}
Source code in portgeo/foliumap.py
def add_gdf(self, gdf, **kwargs):
    """Add a GeoDataFrame layer to the map.

    Args:
        gdf (GeoDataFrame): GeoDataFrame to add.
        **kwargs: Additional arguments for folium.GeoJson.
    """

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

add_geojson(self, data, tooltip_fields=None, tooltip_aliases=None, highlight=True, style_function=None, **kwargs)

Add a GeoJSON layer to the map with optional tooltip and highlight on hover.

Parameters:

Name Type Description Default
data str or dict

Path to the GeoJSON file or GeoJSON data.

required
tooltip_fields list

Fields to include in the tooltip.

None
tooltip_aliases list

Optional aliases for tooltip fields.

None
highlight bool

Whether to highlight geometry on hover.

True
style_function function

Optional base style function.

None
**kwargs

Additional arguments for folium.GeoJson.

{}

Exceptions:

Type Description
TypeError

If data is not a string or dictionary.

ValueError

If data is not a valid GeoJSON.

Source code in portgeo/foliumap.py
def add_geojson(
    self,
    data,
    tooltip_fields=None,
    tooltip_aliases=None,
    highlight=True,
    style_function=None,
    **kwargs,
):
    """Add a GeoJSON layer to the map with optional tooltip and highlight on hover.

    Args:
        data (str or dict): Path to the GeoJSON file or GeoJSON data.
        tooltip_fields (list): Fields to include in the tooltip.
        tooltip_aliases (list): Optional aliases for tooltip fields.
        highlight (bool): Whether to highlight geometry on hover.
        style_function (function): Optional base style function.
        **kwargs: Additional arguments for folium.GeoJson.

    Raises:
        TypeError: If data is not a string or dictionary.
        ValueError: If data is not a valid GeoJSON.
    """
    import geopandas as gpd
    import folium

    if isinstance(data, str):
        gdf = gpd.read_file(data)
        geojson = gdf.__geo_interface__
    elif isinstance(data, dict):
        geojson = data
    else:
        raise TypeError("Data must be a file path or GeoJSON dictionary.")

    # Default style (optional)
    default_style = style_function or (
        lambda f: {"fillOpacity": 0.5, "weight": 1, "color": "gray"}
    )

    # Optional highlight effect on hover
    highlight_fn = (
        (lambda f: {"weight": 3, "color": "yellow", "fillOpacity": 0.2})
        if highlight
        else None
    )

    # Tooltip configuration
    tooltip = (
        folium.GeoJsonTooltip(
            fields=tooltip_fields,
            aliases=tooltip_aliases or tooltip_fields,
            localize=True,
            sticky=True,
            labels=True,
        )
        if tooltip_fields
        else None
    )

    folium.GeoJson(
        data=geojson,
        style_function=default_style,
        highlight_function=highlight_fn,
        tooltip=tooltip,
        **kwargs,
    ).add_to(self)

add_layer_control(self)

Add a layer control to the map.

Parameters:

Name Type Description Default
**kwargs

Additional arguments for folium.LayerControl.

required
Source code in portgeo/foliumap.py
def add_layer_control(self):
    """Add a layer control to the map.

    Args:
        **kwargs: Additional arguments for folium.LayerControl.
    """
    folium.LayerControl().add_to(self)
    folium.plugins.Fullscreen().add_to(self)

add_shp(self, data, **kwargs)

Add a shapefile layer to the map.

Parameters:

Name Type Description Default
data str

Path to the shapefile.

required
**kwargs

Additional arguments for folium.GeoJson.

{}
Source code in portgeo/foliumap.py
def add_shp(self, data, **kwargs):
    """Add a shapefile layer to the map.

    Args:
        data (str): Path to the shapefile.
        **kwargs: Additional arguments for folium.GeoJson.
    """

    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_split_map(self, left='openstreetmap', right='cartodbpositron', **kwargs)

Add a split map to the map.

Parameters:

Name Type Description Default
left str

Name of the left layer. Default is "openstreetmap".

'openstreetmap'
right str

Name of the right layer. Default is "cartodbpositron".

'cartodbpositron'
**kwargs

Additional arguments for folium.TileLayer.

{}
Source code in portgeo/foliumap.py
def add_split_map(self, left="openstreetmap", right="cartodbpositron", **kwargs):
    """Add a split map to the map.
    Args:
        left (str): Name of the left layer. Default is "openstreetmap".
        right (str): Name of the right layer. Default is "cartodbpositron".
        **kwargs: Additional arguments for folium.TileLayer.
    """
    from localtileserver import get_folium_tile_layer
    import os

    if left.startswith("http") or os.path.exists(left):
        layer_left = get_folium_tile_layer(left, overlay=True, **kwargs)
    else:
        layer_left = folium.TileLayer(left, overlay=True, **kwargs)

    if right.startswith("http") or os.path.exists(right):
        layer_right = get_folium_tile_layer(right, overlay=True, **kwargs)
    else:
        layer_right = folium.TileLayer(right, overlay=True, **kwargs)

    sbs = folium.plugins.SideBySideLayers(
        layer_left=layer_left, layer_right=layer_right
    )

    layer_left.add_to(self)
    layer_right.add_to(self)
    sbs.add_to(self)

add_vector(self, data, **kwargs)

Add a vector layer to the map.

Parameters:

Name Type Description Default
data str or GeoDataFrame or dict

Path to the vector file, GeoDataFrame, or GeoJSON data.

required
**kwargs

Additional arguments for folium.GeoJson.

{}
Source code in portgeo/foliumap.py
def add_vector(self, data, **kwargs):
    """Add a vector layer to the map.

    Args:
        data (str or GeoDataFrame or dict): Path to the vector file,
            GeoDataFrame, or GeoJSON data.
        **kwargs: Additional arguments for folium.GeoJson.
    """
    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(
            "Unsupported data type. Please provide a GeoDataFrame, GeoJSON, or file path."
        )