Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Should tm_basemap define the default CRS in plot mode? #975

Open
barryrowlingson opened this issue Dec 3, 2024 · 12 comments
Open

Should tm_basemap define the default CRS in plot mode? #975

barryrowlingson opened this issue Dec 3, 2024 · 12 comments

Comments

@barryrowlingson
Copy link

If I do eg tm_basemap() + tm_shape(d) + tm_polygons() then in plot mode the coordinates are the CRS of d, and if that isn't the CRS of the basemap then the basemap is reprojected. As a raster, when reprojected it can look ugly due to interpolation artefacts.

Instead, should tm_basemap() set the underlying CRS to its CRS (usually web mercator, EPSG:3857) unless overridden, and how would that override be effected?

Should tm_basemap() have is.main and crs arguments like tm_shape, allowing it to be set as the "main" layer and also to be projected to another CRS? Although unlike is.main it shouldn't define the bounding box...

@mtennekes
Copy link
Member

Makes sense.

Suggestion:

  • tm_basemap determines the CRS
  • Unless is.main = TRUE is specified in a tm_shape

Borderline cases:

  • what happens if crs is specified in tm_shape? Should is.main be interpreted as 'TRUE' or should the basemap CRS be used with a message that 'is.mainshould be set to TRUE explicitly ortm_basemap` removed.
  • what happens if the shape is a raster (stars or SpatRaster object) with a different CRS?
  • what happens if multiple basemaps are drawn, each with a different crs? E.g. a basemap and an overlay. Should be very rare, but still nice to have this covered.

@Nowosad
Copy link
Member

Nowosad commented Dec 4, 2024

Should not the action be based on the order of layers? E.g., tm_basemap() + tm_shape(d) + tm_polygons() would use the CRS of the base map, and tm_shape(d) + tm_polygons() + tm_basemap() would use the CRS of the first layer?

@barryrowlingson
Copy link
Author

Yes, with the proviso that tm_shape(d) + tm_polygons() + tm_basemap() wouldn't put the basemap on top of the d shape layer? I think basemaps should always be the lowest layer in a plot. If you want a basemap on top of a data layer then that's not a basemap, that's a raster.

Multiple basemaps in plot mode should appear in specified order (to allow eg a label tile layer on top of a plain layer) , in view mode they get a radio-button selector?

@mtennekes
Copy link
Member

(typing my response to @Nowosad at the same time as @barryrowlingson)

To be honest: no.

In principle, the order of layers determines the plotting order. The exception is tm_basemap, but tm_tiles respects the plotting order. See example:

tm_shape(World) +
	tm_polygons() +
	tm_basemap(NULL) +
	tm_tiles("CartoDB.PositronOnlyLabels") +
	tm_symbols(col = "red")

The plotting order can be changed with the z parameter, but basemaps are by default drawn first

mtennekes added a commit that referenced this issue Dec 12, 2024
mtennekes added a commit that referenced this issue Dec 12, 2024
@mtennekes
Copy link
Member

For the time being I've implemented this:

  • tm_basemap determines the CRS
  • Unless is.main = TRUE is specified in a tm_shape
tm_basemap() + tm_shape(NLD_prov) + tm_polygons() + tm_grid()

image

tm_basemap(NULL) + tm_shape(NLD_prov) + tm_polygons() + tm_grid()

image

Two issues:

  • The plot basemaps still look blurry (resolution still too low?) @rCarto is there anything I can improve? E.g. is it somehow possible obtain higher resolution 'retina' tiles?
  • So far, tile servers are assumed otherwise be in 3857. Once I got tile servers in other CRSs working, but not anymore.

@rCarto
Copy link

rCarto commented Dec 13, 2024

The use of 'retina' tiles is a good solution. But only a few providers offer them (CardoDB, Stadia...).

library(sf)
#> Linking to GEOS 3.11.1, GDAL 3.6.2, PROJ 9.1.1; sf_use_s2() is TRUE
library(maptiles)
nc <- st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
(tile <- get_tiles(nc, provider = "CartoDB.Positron", zoom = 6, 
                   crop = TRUE, forceDownload = TRUE, project = FALSE))
#> class       : SpatRaster 
#> dimensions  : 152, 404, 3  (nrow, ncol, nlyr)
#> resolution  : 2445.985, 2445.985  (x, y)
#> extent      : -9387690, -8399512, 4011415, 4383205  (xmin, xmax, ymin, ymax)
#> coord. ref. : WGS 84 / Pseudo-Mercator (EPSG:3857) 
#> source(s)   : memory
#> colors RGB  : 1, 2, 3 
#> names       : lyr.1, lyr.2, lyr.3 
#> min values  :   143,   157,   170 
#> max values  :   250,   250,   248
prov <- maptiles::get_providers()$CartoDB.Positron
prov$src <- "CartoDB.Positron.2x"
# change server address to get 'retina' tiles
prov$q <- "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png"
(tile2x <- get_tiles(nc, provider = prov, zoom = 6, 
                     crop = TRUE, forceDownload = TRUE, project = FALSE))
#> class       : SpatRaster 
#> dimensions  : 303, 808, 3  (nrow, ncol, nlyr)
#> resolution  : 1222.992, 1222.992  (x, y)
#> extent      : -9387690, -8399512, 4012638, 4383205  (xmin, xmax, ymin, ymax)
#> coord. ref. : WGS 84 / Pseudo-Mercator (EPSG:3857) 
#> source(s)   : memory
#> colors RGB  : 1, 2, 3 
#> names       : lyr.1, lyr.2, lyr.3 
#> min values  :   143,   157,   170 
#> max values  :   250,   250,   248
plot_tiles(tile, adjust = FALSE)

plot_tiles(tile2x, adjust = FALSE)

plot_tiles(tile, adjust = TRUE)

plot_tiles(tile2x, adjust = TRUE)

@mtennekes
Copy link
Member

Thx @rCarto

Could you put this chunk

prov <- maptiles::get_providers()$CartoDB.Positron
prov$src <- "CartoDB.Positron.2x"
# change server address to get 'retina' tiles
prov$q <- "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}@2x.png"

in get_tiles?

It would be great if I can do this in tmap (as user of maptiles)

get_tiles(shp, provider = s, zoom = 6, retina = TRUE)

(where s is an sf/bbox, and s is the server name)

Of course this can only be done when retina tiles are supported. (So perhaps a message/warning in case not?)

@mtennekes
Copy link
Member

This is not critical for the tmap4 release, so it has low urgency.

@mdsumner
Copy link

@barryrowlingson do you have a reprex of the basemap being reprojected that looks bad? I agree the default could be basemap-crs-aligned, but it shouldn't look bad and it's hardly slow anymore. Maybe we can improve how that's happening (sometimes it's about the resampling method or even the matching of resolution of the warp vs the final display)

@mdsumner
Copy link

Another aspect is that basemaps obviously have prerendered resolutions at which they are best displayed, but juggling device size and an implied or specified extent means the final plot rarely aligns to native resolution, it can be better to target a zoom above or below the final resolution, especially if text is present and then resampling algorithm also really matters, and the image text or features at one zoom might be better than the adjacent one for final result. We don't have anything at all in R for helping match or switch between these variants afaik, I've toyed with various things but usually providing specific device dims to the warper is the best approach

@tim-salabim
Copy link

What about a vector tiles background map?

@mdsumner
Copy link

mdsumner commented Jan 20, 2025

thanks to rstats.me discussion with Trevor Davis, this gives the region in pixel coordinates for the current viewport, potentially relative to xscale/yscale (because grid.raster by default preserves native aspect ratio)

## funs from {grid}
deviceLoc(unit(c(0, 1), "npc"), unit(c(0, 1), "npc"), device = T)

so that as n-pixels x and y, gives the right shape to provide the warper for imagery from a web server that has zoom-specific text (and I find "cubic" works well). it's OT here so I won't pursue anymore. I expect a basemap grob could use this to call out to GDAL to get a good looking image, but I that might need two passes (so you know the render ...).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants