r-tmap / tmap

R package for thematic maps
https://r-tmap.github.io/tmap
GNU General Public License v3.0
855 stars 119 forks source link

setting max native zoom automatically #880

Open marine-ecologist opened 3 months ago

marine-ecologist commented 3 months ago

?tm_view mentions passing options to leafletOptions() via leaflet.option=, but I can't seem to get this to work for leaflet::providerTileOptions. Example below:

not working:

library(sf)
library(tmap)
library(leaflet)

# set coord
heron_island_coords <- st_sfc(st_point(c(151.9110, -23.4421)), crs = 4326) |> 
  st_transform(crs = 20355)

#tmap
tmp_map <- tm_basemap("Esri.WorldImagery") +
tm_shape(heron_island_coords) +
  tm_dots() +
tm_view(set.zoom.limits=c(18,30), leaflet.options = providerTileOptions(maxNativeZoom=18,maxZoom=100))

tmp_map

working via tmap_leaflet():

tmp_map |>  tmap_leaflet() |>
  leaflet::addProviderTiles('Esri.WorldImagery', options=leaflet::providerTileOptions(maxNativeZoom=18,maxZoom=100)) 
mtennekes commented 3 months ago

Apparently, we have to pass the zoom limits (set.zoom.limits) not only to options of leaflet(), but also to providerTileOptions in addProviderTiles.

So far so good. However, the maxNativeZoom depends on the tile provider, and needs to be specified manually. Leaving it undefined gives a blank layer, e.g.

tmp_map |>  tmap_leaflet() |>
    leaflet::addProviderTiles('Esri.WorldImagery', options=leaflet::providerTileOptions(maxZoom=100)) 

does not work.

As a user, you always want to be able to zoom in to the most detailed zoom level available. In other words, I see no reason to set the general max zoom to 18, but the max native zoom level to 10 (if 18 is also available).

Relevant post: https://github.com/Leaflet/Leaflet/issues/6316

@tim-salabim how did you deal with this issue in mapview?

tim-salabim commented 3 months ago

We don't use maxNativeZoom at all in mapview. The maxZoom for the basemaps is set to 52 here. I can't quite remember why we set this to 52 exactly, but I vaguely remember that some issues arose if it was set to something higher. The general reason for maxZoom to be set so high is that it sometimes helps to see very small sliver polygons that can be produced by operations like st_intersection et al.

The issue seems to be quite tough, as maxZoom can even vary within basemap providers. Take the following code and zoom into North America somewhere, you will see that you can zoom past 19 and still get some basemap. However, if you zoom somewhere into Africa, it stops at zoom 17 and hence will not enable zooming past that (you can zoom, but no imagery). If you set maxNativeZoom = 17 then you will be able to zoom further into Africa, but at the same time you loose zoom levels 18 & 19 in North America...

library(leaflet)

leaflet() |>
    leaflet::addProviderTiles('Esri.WorldImagery', options=leaflet::providerTileOptions(maxZoom=52, maxNativeZoom = 19)) |> leafem::addMouseCoordinates()

Not sure if there is a good one-fits-all solution here...

marine-ecologist commented 2 months ago

Would it be feasible to set a max zoom internally for the common basemap providers?

providers_max_zoom <- list(
  "OpenStreetMap" = 19,
  "CartoDB.Positron" = 19,
  "Stamen.Toner" = 20,
  "Esri.WorldImagery" = 17
)
tim-salabim commented 2 months ago

That's already done internally on the JavaScript side by the leaflet.providers package if I'm not mistaken

mtennekes commented 2 months ago

Good idea @marine-ecologist ! I can include such a list in tmap, but it is preferable to do maintain such a list somewhere upstream (JS side). Can't find it in the leaflet.providers @tim-salabim, at least from the R side.

I've added max.native.zoom to tm_tiles:

tm_basemap("Esri.WorldImagery", max.native.zoom = 18) +
    tm_shape(heron_island_coords) +
    tm_dots() +
    tm_mouse_coordinates() +
    tm_view(set.view = 16, set.zoom.limits=c(2,20))
tim-salabim commented 2 months ago

This https://github.com/leaflet-extras/leaflet-providers/blob/master/leaflet-providers.js#L78-L1217

Just to mention it here, maybe python xyzservices has a solution to the maxNativeZoom issue? @martinfleis https://github.com/geopandas/xyzservices

martinfleis commented 2 months ago

Not sure I follow what the actual issue is here. xyzservices stores metadata for min_zoom and max_zoom. When this gets consumed by folium (Python leaflet.js wrapper), these get used within the map unless a user overrides any of them manually. maxNativeZoom is, by default, equal to max_zoom but you can also override that.

We don't use maxNativeZoom at all in mapview.

I believe that if you don't set it, tiles at higher zoom levels just disappear. If you do, the tiles are autoscaled (blurred).

tim-salabim commented 2 months ago

I believe that if you don't set it, tiles at higher zoom levels just disappear. If you do, the tiles are autoscaled (blurred).

In theory, yes. But if you take the example from https://github.com/r-tmap/tmap/issues/880#issuecomment-2131154655 you will see that it only autoscales in regions within the map where the map provider actually has tiles until maxNativeZoom (North America in this example). If there are no tiles at this depth (as in Africa in the example), then can zoom until maxNativeZoom is reached, but you get the same behaviour as if maxNativeZoom was not set (i.e. tiles disappear; no autoscaling). Hence, I think given the current implementation of the JavaScript code in leaflet(Providers), there is no consistent way of setting maxNativeZoom to get identical behaviour everywhere. Does that clarify the issue at hand @martinfleis ?

marine-ecologist commented 2 months ago

@tim-salabim - I hadn't realised maxNativeZoom was dynamic. One solution/workaround implemented elsewhere is Leaflet.TileLayer.Fallback that replaces missing Tiles (404 error) with scaled lower zoom Tiles.

I've tried implementing this via leaflet and htmltools (with a West Africa coast) as a Dependency but to with limited success either inline or via unpkg:

library(leaflet)
library(htmlwidgets)
library(htmltools)

# Create the JavaScript dependency
js_dep <- htmlDependency(
  name = "leaflet-tilelayer-fallback",
  version = "1.0.4",
  src = c(href = "https://unpkg.com/leaflet.tilelayer.fallback@1.0.4/dist/"),
  script = "leaflet.tilelayer.fallback.js"
)

# Create the leaflet map
map <- leaflet() %>%
  addProviderTiles(
    providers$Esri.WorldImagery,
    options = providerTileOptions(maxNativeZoom = 18, maxZoom = 100)
  ) %>%
  addMarkers(lng = 10.54925, lat = -51.16226) %>%
  onRender("
    function(el, x) {
      var map = this;
      L.tileLayer.fallback('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        minNativeZoom: 0,
        maxNativeZoom: 20,
        maxZoom: 100
      }).addTo(map);
    }
  ")

# Attach the JavaScript dependency to the map
map <- htmltools::attachDependencies(map, js_dep)

map
martinfleis commented 2 months ago

In theory, yes. But if you take the example from https://github.com/r-tmap/tmap/issues/880#issuecomment-2131154655 you will see that it only autoscales in regions within the map where the map provider actually has tiles until maxNativeZoom

That is not what is happening. The issue with these specific ESRI tiles is that they do provide tiles for higher zoom levels but those images just say "no data...". If you test on something else, like OpenStreetMap.HOT, it actually behaves the way I described. The issue you are facing here is with tiles not software.

tim-salabim commented 2 months ago

@martinfleis thanks for the clarification. I think there's not much we can do here (apart from utilising @marine-ecologist "s suggestion using Leaflet.TileLayer.Fallback ), though, for me, this is not high priority at the moment.