r-tmap / tmap

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

'shapeless' map (e.g. basemap only) specs #762

Open mtennekes opened 1 year ago

mtennekes commented 1 year ago

How should users be able to set the specs, like crs, bbox, zoom level, etc. for shapeless maps, by which I mean maps without using 'shape' objects (sf,stars,terra )?

So far, the only example I could think of is a basemap without additional data layers:

Screenshot 2023-08-16 at 13 31 41 Screenshot 2023-08-16 at 13 32 46

Few options:

  1. (= the option illustrated above) Set these specs as options.
  2. Same as 1, but in addition make them available in tm_view and tm_plot. This is the same behaviour as in tmap3. tm_plot is new, and added to allow plot-mode specific options.
  3. Specify a tm_shape object without the spatial object itself. So e.g. tm_shape(bbox = "Amsterdam") + tm_basemap()
  4. Specify it in the tm_basemap layer itself, as layer-specific properties.

What are your thoughts/options/ideas? (@Nowosad @Robinlovelace others)

Nowosad commented 1 year ago

I am leaning toward option 4 (my thinking is that the tm_basemap is somewhat similar to tm_graticules), but I also think that option 3 makes a lot of sense.

Nowosad commented 1 year ago

After a quick nap, I like option 3 better. IT seems to be more consistent with the rest of the tmap code.

mtennekes commented 1 year ago

Great to see what naps can do, not just with the little ones among us:-)

I also thought of a new option:

  1. Introduce tm_map where crs, bbox, and zoom levels (min, max and current) are determined. In that case, tm_shape will only be used to specify the spatial object, and optionally is.main in case there are multiple objects with different bboxes. In case tm_map is not called, the map properties are extracted from the main shape. In case tm_map is used, it will determine the properties of the produced map.

Still have to think about the pros and cons, but so far:

Pro:

Cons:

Nowosad commented 1 year ago

I think tm_map() would add unnecessary complexity (for the users point of view) for fairly small benefits.

Robinlovelace commented 1 year ago

No strong opinions but from a quick read

tm_shape(bbox = "Amsterdam") + tm_basemap()

Sounds logical to me.

mtennekes commented 1 year ago

For now I went with the tm_shape option, but in essence (and implementation wise) a tm_shape call without shape is still a tm_options call. That means that:

tm_basemap() + tm_shape(bbox = "Amsterdam") 

also works. And also without tm_basemap:

tm_shape(World) + tm_polygons() + tm_shape(bbox = "Amsterdam") 

We could prevent this to bahavior, and make tm_shape 'connected' to tm_basemap, but then we run into a new problem with a common situation, namely where you are in view mode and do not explicitly call tm_basemap.

So, I am okay with this choice, but from a purely conceptually point of view, I'd like tm_map better. However, I also increase with Jakub that it may add complexity (and confusion) to some users.

Furthermore, while implementing this, I encountered two other related issues:

  1. You can set the zoom level in tm_basemap. This was already implemented a while ago. The only purpose is to decide on which zoom level to render the tiles in plot mode. However, this could clash with the zoom level set via tm_shape(or tm_map), which is via the argument set.view (see next issue about the naming). For now, I applied this simple rule: if the zoom level is specified in tm_basemap, and not in tm_shape (or tm_map), use that.
  2. There are three arguments in v3 tm_view that determine the map view: set.bounds, set.view and set.zoom.limits. The input specifications and the names are directly taken from the upstream leaflet functions (though the names are in camel case). Problem: I don't find these user friendly. I'd rather change set.view to zoom. Via set.view you can also set the center point in lat/lon coordinates, but this also follows from the bounding box. Furthermore, set.zoom.limits can be changed to zoom.limits, and set.bounds probably can be changed to bound (logical that determined whether to bound the map or not.
  3. Also related, I'm not sure to what extend these options also apply to other not-yet-implemented interactive modes, for instance rayshader or deck.gl.

Ideas / opinions?

Robinlovelace commented 1 year ago

So a tm_shape() call with no spatial object creates a map with only a basemap?

I do think having a tm_basemap() function is useful, if only to know where to put basemap argument like starting zoom, max zoom, etc that can feed into the interactive mapping functions below.

I agree that the zoom level in tm_basemap() should take precedence.

Regarding 2, I agree they are not particularly intuitive. Could those updated argument names go into tm_basemap() and tm_view (which implies tm_basemap(), no?) be deprecated?

Re. 3 I think getting it working from currently working interactive mode and then iterate and ask others to implement for other interactive back-ends is reasonable.

Just my quickfire thoughts after quick read, hope useful.

mtennekes commented 1 year ago

Thx Robin, certainly useful!

So a tm_shape() call with no spatial object creates a map with only a basemap?

In view mode, yes, but in plot mode nothing happens except the message "Nothing to show". This is because basemap(s) are by default added in view mode, but not in plot mode.

In essence, tm_shapewithout spatial object is just setting tmap options (just like tm_options / tm_layout). At least, in the current implementation, which we are reconsidering in this discussion.

I do think having a tm_basemap() function is useful, if only to know where to put basemap argument like starting zoom, max zoom, etc that can feed into the interactive mapping functions below.

I agree that the zoom level in tm_basemap() should take precedence.

Regarding 2, I agree they are not particularly intuitive. Could those updated argument names go into tm_basemap() and tm_view (which implies tm_basemap(), no?) be deprecated?

Yes, tm_basemap() is definitely useful, especially for choosing the tile server. We can specify the starting/min/max zoom in tm_basemap, but these zoom levels are also relevant for interactive maps without basemap. So that could make things a bit confusing, because then you would have to do something like tm_basemap(server = NULL, zoom = 9, zoom.max = 12)

Re. 3 I think getting it working from currently working interactive mode and then iterate and ask others to implement for other interactive back-ends is reasonable.

Yes, true. The point is, if we can guess a little bit ahead what would be required, we can already anticipate, so that we don't have to change the user interface (function/arguments) after tmap4 is on cran.