python-visualization / folium

Python Data. Leaflet.js Maps.
https://python-visualization.github.io/folium/
MIT License
6.93k stars 2.23k forks source link

Fix FitOverlays when using multiple Tilesets #1971

Closed steeevin88 closed 5 months ago

steeevin88 commented 5 months ago

Describe the bug To allow maps to switch between different TileSets, we can add each TileLayer to its own FeatureGroup. However, when combined with FitOverlays (ex. folium.FitOverlays().add_to(m)), FitOverlays breaks. Upon inspecting the console:

image

To Reproduce https://nbviewer.org/github/steeevin88/fileoverlay_fix/blob/main/map.ipynb

import folium

m = folium.Map(location=[37.5665, -120.9780], zoom_start=8, tiles=None)

folium.TileLayer(
  tiles="OpenStreetMap",
  name='OpenStreetMap',
  control=False,
  opacity=1
).add_to(folium.FeatureGroup(name="US Topo", overlay=False, show=True).add_to(m))

folium.TileLayer(
  tiles="Cartodb Positron",
  name="Cartodb Positron",
  control=False,
  opacity=1
).add_to(folium.FeatureGroup(name="US Imagery Topo", overlay=False, show=False).add_to(m))

folium.Marker(
  location=[33.3382, -117],
  icon=folium.Icon(color="red", icon="info-sign")
).add_to(folium.FeatureGroup(name="Marker 1", show=True).add_to(m))

folium.Marker(
  location=[33.3382, -130],
  icon=folium.Icon(color="red", icon="info-sign")
).add_to(folium.FeatureGroup(name="Marker 2", show=True).add_to(m))

# Add LayerControl to toggle between layers
folium.LayerControl(collapsed=False).add_to(m)

# Convert the Folium map to HTML and display it in the notebook
folium.FitOverlays().add_to(m)
m

Expected behavior Both the ability to switch between TileLayers and the functionality of FitOverlays should work.

Environment (please complete the following information):

Additional context I realize that the option to switch between multiple TileLayers isn't that common and most maps will probably use a single tileset. However, for maps that do want the option of multiple tilesets + use the "workaround" above, FitOverlays doesn't work. I believe it's because TileLayers don't have latitude or longitude, so when added to a FeatureGroup they lead to the error above.

Possible solutions I think it should just be a few lines of JavaScript actually! We'd modify here: https://github.com/python-visualization/folium/blob/230e3e34cc286cb65db479d7d906de9814506821/folium/map.py#L657-L660

I did some extra Python code for easy testing; this is also in the notebook above

import folium
from jinja2 import Template

class CustomFitOverlays(folium.FitOverlays):
    _template = Template(
        """
        {% macro script(this, kwargs) %}
        function customFlyToBounds() {
            let bounds = L.latLngBounds([]);
            {{ this._parent.get_name() }}.eachLayer(function(layer) {
                // Custom JavaScript that skips TileLayers...
                if (layer._tiles !== undefined || 
                (layer._layers && Object.values(layer._layers).some(l => (l._tiles !== undefined)))) {
                    return;
                }
                if (typeof layer.getBounds === 'function') {
                    bounds.extend(layer.getBounds());
                } else if (typeof layer.getLatLng === 'function') {
                    bounds.extend(layer.getLatLng());
                }
            });
            if (bounds.isValid()) {
                {{ this._parent.get_name() }}.{{ this.method }}(bounds, {{ this.options|tojson }});
            }
        }
        {{ this._parent.get_name() }}.on('overlayadd', customFlyToBounds);
        {%- if this.fit_on_map_load %}
        customFlyToBounds();
        {%- endif %}
        {% endmacro %}
        """
    )

CustomFitOverlays(fly=True, max_zoom=15).add_to(m)
m

It's primarily these lines

 if (layer._tiles !== undefined || 
                (layer._layers && Object.values(layer._layers).some(l => (l._tiles !== undefined)))) {
                    return;
                }

This works, as shown in the Jupyter Notebook, but is there a better way to discern TileLayers?

Conengmo commented 5 months ago

Hi @steeevin88, I looked at this a bit, but something that puzzles me is why add the tilelayers to a feature group? If I add the tilelayer to the map directly, I don't have this issue with fitoverlays.

m = Map(tiles=None)
TileLayer("openstreetmap").add_to(m)
TileLayer('Cartodb Positron').add_to(m)

^^^ This works

m = Map(tiles=None)
TileLayer("openstreetmap", control=False).add_to(
    FeatureGroup("openstreetmap fg", overlay=False).add_to(m)
)
TileLayer('Cartodb Positron', control=False).add_to(
    FeatureGroup("cartodb fg", overlay=False).add_to(m)
)

^^^ this doesn't, but I don't see why this is necessary.

Thanks for your thorough issue report! I hope you can say something about this, and then let's see how we continue on this!

steeevin88 commented 5 months ago

Hi @Conengmo; I appreciate you looking into this!

I totally agree with you; I have no clue why I added the TileLayer to a FeatureGroup. I think I added "control=False" to my TileLayers at some point and so without adding it to a FeatureGroup, the tilesets wouldn't appear in the LayoutControl... but removing that parameter makes it visible; whoops.

Adding just the tile layers directly allows FitOverlays to work just fine, so this issue can probably be closed unless you have any other thoughts. Thanks for your help!

Conengmo commented 5 months ago

Good to hear it's solved, thanks for getting back! Since the 'tilelayers in featuregroups' is not really common or expected behavior, I think we can indeed close this one. If another issue with fitoverlays or something else comes up feel free to open a new issue!