6

I am using R, RStudio and the leaflet package to visualise a map.

I would like to get the the min and max lat-longs of of the bounding box of a leaflet object. I think this can be done using Shiny (by using something like input$mapobj_bounds) but is there a non-shiny method to do this.

m <- leaflet(width=500,height=400) %>% 
   addTiles() %>% 
   setView(lng = -0.106831, lat = 51.515328, zoom = 18) %>%
   addCircleMarkers(lng = -0.106831, lat = 51.515328)

What i need is a function to get the bounding box using the input argument m.

Can this be done?

Also, the parameter values when looking into the object m look incorrect.

e.g.

> m$x$limits
$lat
[1] 51.51533 51.51533

$lng
[1] -0.106831 -0.106831

EDIT

I think the javascript function map.getBounds() may be of help here...as suggested here (Get the bounding box of the visible leaflet map?), but do not know how to apply this to our problem. Any help on this would be much appreciated.

Tonio Liebrand
  • 17,189
  • 4
  • 39
  • 59
h.l.m
  • 13,015
  • 22
  • 82
  • 169
  • yes i also posted a solution with `map.getBounds()` an hour ago but deleted it as i didnt find a way to return the values as R variables. I only managed to produce popups :) I can undelete it if you like. Concerning a function f(m) that returns the bounds to R, i think one would need the ratio scales for the zoom levels, like there exists for google maps: https://gis.stackexchange.com/questions/7430/what-ratio-scales-do-google-maps-zoom-levels-correspond-to. But i didnt find it for leaflet,.. – Tonio Liebrand May 25 '17 at 12:47
  • Could they be written to csv as an output from an html once the object has been saved?? But yes please un-delete the answer as i would be keen to learn more javascript too – h.l.m May 25 '17 at 12:52
  • The results of map.getBounds() (as demonstrated nicely by @BigDataScientist) change as you resize the Viewer window. This indicates that the bounding box does not depend entirely on your `m` object. Even if you were able to output the result from the html, it would only give you the results for that particular browser and it's settings. – Jeremy Voisey May 30 '17 at 18:18

3 Answers3

10

If you adapt Jeremys original answer a bit you can actually do it without javascript:

Reproducible example:

library(magrittr)
library(leaflet)

m <- leaflet(width = 500,height = 400) %>% 
  addTiles() %>% 
  setView(lng = -0.106831, lat = 51.515328, zoom = 18) %>%
  addCircleMarkers(lng = -0.106831, lat = 51.515328)
m
getBox <- function(m){
  view <- m$x$setView
  lat <- view[[1]][1]
  lng <- view[[1]][2]
  zoom <- view[[2]]
  zoom_width <- 360 / 2^zoom
  lng_width <- m$width / 256 * zoom_width
  lat_height <- m$height / 256 * zoom_width
  return(c(lng - lng_width/2, lng + lng_width/2, lat - lat_height/2, lat + lat_height/2))
}
getBox(m)

In shiny you can simply you use: input$MAPID_bounds

Reproducible example:

library(shiny)
library(leaflet)
library(magrittr)

app <- shinyApp(

  ui = fluidPage(leafletOutput('myMap')),

  server = function(input, output) {

    output$myMap = renderLeaflet({
      leaflet() %>% 
        addTiles() %>% 
        setView(
          lng = 50, 
          lat = 10, 
          zoom = 17
        )
    })

    observeEvent(input$myMap_bounds, {
      print(input$myMap_bounds)
    })

  }
)

for more info see here: https://rstudio.github.io/leaflet/shiny.html.

Here a javscript version (initial workaround). For the better version, see above.

  leaflet() %>% addTiles()  %>% 
  setView(lng = -0.106831, lat = 51.515328, zoom = 18) %>%
  addEasyButton(easyButton(
    states = list(
      easyButtonState(
        stateName="unfrozen-markers",
        icon="ion-toggle",
        title="Get Bounding box",
        onClick = JS("
                     function(btn, map) {
                        alert(map.getBounds().getEast());
                        alert(map.getBounds().getWest());
                        alert(map.getBounds().getNorth());
                        alert(map.getBounds().getSouth());
                     }")
      )
    )
  )
)
Tonio Liebrand
  • 17,189
  • 4
  • 39
  • 59
3

Thanks to @BigDataScientist's answer for pointing out, that width & height are available!

It is possible to calculate the bounding boxes, as long as you know the leaflet widget's dimensions. See leafletjs.com/examples/zoom-levels

Given that this is specified with leaflet(width=500,height=400), this will work.

if (is.null(m$width) | is.null(m$height)) {
    print("Leaflet width and height must be speciied")
} else {
       width <- m$width 
       height <- m$height 
       zoom <- m$x$setView[[2]]
       lng <- m$x$setView[[1]][2]
       lat <- m$x$setView[[1]][1]
       lng_width <- 360 * width / 2^(zoom + 8)
       lng_east <- lng - lng_width/2
       lng_west <- lng + lng_width/2
       lat_height <- 360 * height * cos(lat/180 * pi) / 2^(zoom + 8)
       lat_north <- lat + lat_height/2
       lat_south <- lat - lat_height/2
}

> lng_east
[1] -0.1081721
> lng_west
[1] -0.1054899
> lat_north
[1] 51.516
> lat_south
[1] 51.51466

Comparing to @BigDataScientist, this gives the same answer as map.getBounds to 3 decimal places.

enter image description here

EDIT I based my answer on the documentation from leaflet referenced. It would seem that this is a simplification. I have added the cos(lat/180 * pi) term which improves accuracy. For example, this now gives north-boundary of 51.516, which is only a difference of 0.0000029 from leaflet's 51.51599707.

I have tested this at a few different latitudes and zooms. Accuracy decreases at lower zoom levels.

Jeremy Voisey
  • 1,257
  • 9
  • 13
  • you could use m$height and m$width given the m in the question. See the edit in my post, if its unclear. Feel free to add it to your answer, you were the one coming incredibly close to it :) – Tonio Liebrand May 30 '17 at 22:37
  • Thanks for pointing this out. I was so obsessed with finding the viewer pane's dimensions, I missed that width and height of leaflet were specified in the original question! – Jeremy Voisey May 30 '17 at 23:23
  • it also took me a while, as i made my own small leaflet map for testing and did not specify width and length in there. – Tonio Liebrand May 31 '17 at 08:39
  • I've done some further testing. I managed to improve the accuracy, but there is more work needed at lower zoom levels. – Jeremy Voisey Jun 05 '17 at 13:33
  • Thank you for the additional accuracy by using the cos term...can you please explain the `zoom + 8` part please? – h.l.m Jun 05 '17 at 15:18
  • I simplified the calculation. Part of which was dividing by (256 x 2^zoom). As 256 = 2^8, this becomes 2^8 x 2^zoom = 2^(zoom + 8) – Jeremy Voisey Jun 05 '17 at 15:21
  • It should be possible to increase accuracy further, if someone has time, by reverse engineering leaflet.js to see exactly how they do the calculation. – Jeremy Voisey Jun 05 '17 at 15:24
1

This is an old question but it recently [29 Apr 2022] assisted me in finding a solution to a problem I was faced with. To say thanks I combined the approaches of @TonioLiebrand and @JeremyVoisey into a single self-contained (neatly rounded-off) function as follows:

# Self-contained function to calculate 'Leaflet Map Widget' Bounding Box co-ordinates ...
"f.calc.leaflet.bounding.box.coords" <- function(objLeafletMap=NULL) {
  FUNC_ID_SHORT <- "fCLBBC"; FUNC_ID_FULL <- "f.calc.leaflet.bounding.box.coords";
  boundNorth_ <- NULL; boundWest_ <- NULL; boundSouth_ <- NULL; boundEast_ <- NULL;
  if (is.null(objLeafletMap) || is.null(objLeafletMap$width) || is.null(objLeafletMap$height)) {
    base::message(base::paste0(" --> ", FUNC_ID_SHORT, " - Line 4 ", "| LEAFLET MAP WIDTH & HEIGHT NOT SPECIFIED ( FUNC ID: '", FUNC_ID_FULL, "' )."))
    base::stop(base::paste0(" --> ", FUNC_ID_SHORT, " - Line 5 ", " | Function Execution Terminated [ reason: REQUIRED PARAMS are NULL ] !!"))
  } else {        
    # Extract Leaflet Map Widget 'x' property list values ( i.e. [center 'lat' & 
    # 'lon'], 'zoom' and 'options' widget property values )...
    view_ <- objLeafletMap$x$setView; zoom_ <- view_[[2]]; 
    lon_ <- view_[[1]][2]; lat_ <- view_[[1]][1];
    
    # Extract Leaflet Map Widget 'width' and 'height' values ...
    width_ <- objLeafletMap$width; height_ <- objLeafletMap$height;
    
    # Calculate Leaflet Map Widget peripheral extent in co-ordinate dimensions ...
    lon_width_ <- 360 * width_ / 2 ^ (zoom_ + 8);
    lat_height_ <- 360 * height_ * cos(lat_ / 180 * base::pi) / 2 ^ (zoom_ + 8);
    
    # Calculate Leaflet Map Widget peripheral extent ( i.e. Bounding Box ) in co-ordinate values ...
    boundEast_ <- lon_ + lon_width_ / 2; boundWest_ <- lon_ - lon_width_ / 2;
    boundNorth_ <- lat_ + lat_height_ / 2; boundSouth_ <- lat_ - lat_height_ / 2;
  }
  return(list(top=boundNorth_, right=boundEast_, bottom=boundSouth_, left=boundWest_, zoom=zoom_))
}

This might be a bit of an overkill for some, but might also be a boon for others looking for a quick [time-critical] solution (as I did). Simply copy & paste the function into your R script, and once the function is read into your R Session Memory extract your Leaflet Map Widget bounding co-ordinates as follows:

# Simply call the function from wherever you need in your R script ...
mapBounds <- f.calc.leaflet.bounding.box.coords(m);   # <- 'm' == Leaflet Map Widget (with 'width' and 'height' defined) !!


# ... and extract the results as follows:
mapBounds$top
> -5.83050217387398

mapBounds$right
> 38.25046875 

mapBounds$bottom
> -40.209497826126 

mapBounds$left
> -9.21046875

I also added the zoom value as an output (because it seems wasteful to compute it inside the function but not return it [as a result] to the function call). So now you can easily easily get the zoom value by calling ...

mapBounds$zoom
> 5

... after every map zoom change - if you really, really need to do that.

Lastly, I concur with @JeremyVoisey, there is an accuracy issue with the results from this approach - but this code snippet was sufficient in resolving the problem I had (and I was a bit time-pressed) ... so I didn't look into fixing the accuracy issue at the time.

SilSur
  • 495
  • 1
  • 6
  • 21