tmap icon indicating copy to clipboard operation
tmap copied to clipboard

setting max native zoom automatically

Open marine-ecologist opened this issue 1 year ago • 11 comments

?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)) 

marine-ecologist avatar May 23 '24 19:05 marine-ecologist

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?

mtennekes avatar May 25 '24 07:05 mtennekes

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...

tim-salabim avatar May 25 '24 09:05 tim-salabim

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
)

marine-ecologist avatar Jun 06 '24 19:06 marine-ecologist

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

tim-salabim avatar Jun 06 '24 19:06 tim-salabim

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))

mtennekes avatar Jun 10 '24 19:06 mtennekes

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

tim-salabim avatar Jun 11 '24 06:06 tim-salabim

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).

martinfleis avatar Jun 11 '24 12:06 martinfleis

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 ?

tim-salabim avatar Jun 11 '24 15:06 tim-salabim

@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/[email protected]/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

marine-ecologist avatar Jun 11 '24 19:06 marine-ecologist

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.

martinfleis avatar Jun 11 '24 20:06 martinfleis

@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.

tim-salabim avatar Jun 12 '24 08:06 tim-salabim