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

Can’t get a crisp basemap to display #1041

Closed
cedricr opened this issue Feb 17, 2025 · 10 comments
Closed

Can’t get a crisp basemap to display #1041

cedricr opened this issue Feb 17, 2025 · 10 comments

Comments

@cedricr
Copy link

cedricr commented Feb 17, 2025

I’m not able to generate a clean looking basemap.

Using tmap, I get distorted map elements, the text is unreadable.

Image

Using maptiles directly, everything is fine

Image

It looks like there’s some unwanted reprojection and/or scaling somewhere, but whatever I’ve tried, I could not improve it.

It’s not specific to the OpenStreetMap tileset either.

Using tmap 4.0.

library(tmap)
library(sf)
library(maptiles)

nc = st_read(system.file("shape/nc.shp", package = "sf"))

# This map has distorted text

tm_basemap("OpenStreetMap") +
  tm_crs(3857) +
  tm_shape(nc) # reprojecting the vector data to 3857 won’t help either

# But this map is correct

plot_tiles(
  get_tiles(
    nc,
    provider = "OpenStreetMap",
    zoom = 7,
    crop = TRUE,
    forceDownload = TRUE,
    project = FALSE
  ),
  adjust = FALSE
)
@mtennekes
Copy link
Member

Known issue, see also #603 and #1042
It's probably related to resealing indeed, perhaps also interpolation.

For images like this when the resolution is really low, the only 'fix' is to use the exact same resolution so that resealing is not needed. This is not easy in tmap because it affects all margins, and moreover is graphics device dependent.

The best option is to use so-called Retina tiles, which are rendered in much higher resolution. They are not implemented in tmap yet, but are already in maptiles I'll implement this in tmap soon

@cedricr
Copy link
Author

cedricr commented Feb 18, 2025

Retina tiles will only (maybe) fix it for high-resolution tiles providers no ? So that won’t help for the OSM tiles.

Zoom level 7 was indeed a bit low in my example, so even the plot_tiles version was a bit blurry. If I switch to zoom level 8, plot_tiles becomes perfectly crisp whereas tmap output is unreadable

Image

Image

With a tile server, I would expect not to have to choose the zoom level, but probably the dpi instead if it can’t be autodetected, and have tmap automatically pull the tiles with the optimal zoom level, so that there’s no resizing whatsoever. Easy to say, probably very hard to do !

@mtennekes
Copy link
Member

Just found the issue with the difference in sharpness! Via maptiles the tiles are obtained in 3857, but tmap reprojects them to 4326. In view mode, the raster plot crs was already 3857, so I'll have to do this for plot mode as well. I'll let you know when that is fixed.

With a tile server, I would expect not to have to choose the zoom level, but probably the dpi instead if it can’t be autodetected, and have tmap automatically pull the tiles with the optimal zoom level, so that there’s no resizing whatsoever. Easy to say, probably very hard to do !

If it works correctly, you don't need to choose a zoom level, because it is based on a simple heuristic, which is not based on resolution but rather on real-world map scale.

Regarding the native tile resolution: maptiles::plot_tiles has an argument adjust that matches the tile resolution with the graphics device resolution. Could be useful for tmap as well, but as said, the layout of tmap is quite complex (because panels, axis labels, facets etc. are taken into account).

Strongly related topic: #914

@mtennekes
Copy link
Member

mtennekes commented Feb 18, 2025

Seems to work now. Just started a new branch "crispy". Feel free to test already.

tm_basemap("OpenStreetMap") +
	tm_shape(nc)

Image

Some open questions:

  1. The current default in plot mode is still 4236. That is without basemaps, so tm_shape(World) + tm_polygons(). But when basemaps are added, it switches to 3857. Is this a good thing @Nowosad or shall we always render in 3857 for consistency? Regarding backwards compatibility with CRAN 4.0 we could also opt for 4236 and force users to set it to 3857. My own preference would be always 3857, with the recommendation message to enable 'auto crs'. To capture backwards compatibility we could also inform users that the default crs for unprojected data is changed to 3857. What do you think?
  2. I had to crop the shape, because otherwise the aspect ratio would be 1/10. It's still hard-coded, at ymin = -75 and ymax = 85, which gives this:
tm_shape(World) + tm_borders() + tm_basemap("OpenStreetMap")

Image

I need to add a tmap option for this, probably something like bbox_crop. Are the limits -75 to 85 okay or do you prefer other values?
Another thing to do is to set inner.margins to 0 when basemaps are used.

@Nowosad
Copy link
Member

Nowosad commented Feb 20, 2025

Hi @mtennekes

  1. Would it not be better to use the input data crs by default (so World is 4326), but change to 3857 when tm_basemap() is used?
  2. Do we need another option (like bbox_crop). Would not bbox in tm_shape enough (or some ... arguments in this function)?

(Side note: I updated tmap using the crispy branch, but the nc map is still blurry on my computer)

@mtennekes
Copy link
Member

Just merged the crispy branch to master.

  1. Yes, that is how it's implemented now.
  2. I have added the option limit_latitude_3857, just for cropping in 3857 in plot mode. Otherwise the user needs to use bbox in tm_shape everytime a basemap is added to a world map. (Without it: plot(st_transform(World, 3857)))

How blurry is your nc map? I get still the same sharpness as my last image (and the same as the 2nd one from the opening post). It should be similar. If not, we'll have to figure out what causes it.

The retina tiles look sharper, e.g. "CartoDB.Positron".

I've added tmap_providers() and .tmap_providers. Hopefully they are useful.

@cedricr
Copy link
Author

cedricr commented Feb 21, 2025

Hi @mtennekes,

The crispy version is much better, thanks a lot ! The reprojection problem seems to be solved, so there’s no more distorsion that I can see.

But… it’s not as good as maptiles output, although I think I’ve just understood why.

To test it, I’ve done a short Quarto page

---
format: 
  html:
    page-layout: full
    cache: false
knitr:
  opts_chunk:
    dev: ragg_png
    out-width: 1200px
execute:
  echo: false
  warning: false
---
::: {.column-screen}

```{r}
library(tmap)
library(sf)
library(maptiles)

nc <- st_read(system.file("shape/nc.shp", package = "sf"), quiet = TRUE)
```

```{r}
tm_basemap("OpenStreetMap", zoom = 7) +
  tm_shape(nc) +
  tm_layout(
    inner.margins = c(0, 0, 0, 0),
    outer.margins = c(0, 0, 0, 0)
  )
```

```{r}
plot_tiles(
  get_tiles(
    nc,
    provider = "OpenStreetMap",
    zoom = 7,
    crop = TRUE,
    forceDownload = TRUE,
    project = FALSE
  ),
  adjust = FALSE,
  box = TRUE
)
```
:::

And here are the PNG generated

tmap:
Image

maptiles:
Image

If you look at them at 100 %, you’ll see that the maptiles one is much better.
Looking at the file sizes, tmap’s one is 300k, maptiles is 628k… And that’s when I discovered that plot_tiles inherits a smooth argument from terra’s plotRGB – (logical. If TRUE, smooth the image when drawing to get the appearance of a higher spatial resolution) – and it’s TRUE by default.

And indeed, if I pass smooth=FALSE to plot_tiles, I get nearly the exact same images from both (same “low” quality, same filesize).
If I increase the dpi globally in Quarto, by adding the fig-dpi: 300 html option to the yaml header, I then get two undistinguishable high quality PNGs, around the same size as the initial maptile version.

TLDR: tmap might benefit from a "smooth" option when rendering rasters.

@mtennekes
Copy link
Member

Great find! Thx @cedricr

I've enabled 'interpolate' for basemaps (was disabled since #892), but for basemaps it does improve the perceived sharpness.

This 'interpolate' goes into grid::rasterGrob. Not sure if terra also uses grid graphics, but to me the results seem similar. Please test.

@cedricr
Copy link
Author

cedricr commented Feb 21, 2025

@mtennekes yes ! It works perfectly now, thank you so much !

I’ve also tried getting a raster with {basemaps}, and as long as I add the interpolate option, the display is as good with tmap than it is with terra.

(unrelated: it seems that opt_tm_rgb is not exported, I had to ::: it).

library(basemaps)
data(ext)
set_defaults(map_service = "osm", map_type = "streets")
map <- basemap_terra(ext)

# plotting with Terra
terra::plotRGB(map)

# plotting with tmap 
library(tmap)
tm_shape(map) + tm_rgb(options = tmap:::opt_tm_rgb(interpolate = TRUE))

@mtennekes
Copy link
Member

You're welcome! Yes, I also notices we forgot to export opt_tm_rgb. Have done it, so in the next commit it will be there.

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

No branches or pull requests

3 participants