7

Following on from this question, I am looking to save and download a leaflet map as a png or jpeg image. I have the following code but I keep getting an error.

ui <- fluidPage(
  leafletOutput("map"),
  downloadButton("dl")
)

server <- function(input, output, session) {
  output$map <- renderLeaflet({
    leaflet() %>% 
      addTiles()
  })

  output$dl <- downloadHandler(
    filename = "map.png",

    content = function(file) {
      mapshot(input[["map"]], file = file)
    }
  )
}

shinyApp(ui = ui, server = server)

The error I get when I try to download (by clicking the button) is

Warning: Error in system.file: 'package' must be of length 1
Stack trace (innermost first):
    65: system.file
    64: readLines
    63: paste
    62: yaml.load
    61: yaml::yaml.load_file
    60: getDependency
    59: widget_dependencies
    58: htmltools::attachDependencies
    57: toHTML
    56: <Anonymous>
    55: do.call
    54: mapshot
    53: download$func [#11]
     4: <Anonymous>
     3: do.call
     2: print.shiny.appobj
     1: <Promise>
Error : 'package' must be of length 1

Bonus points if you can tell me how to get this working with leafletProxy.

nathaneastwood
  • 3,664
  • 24
  • 41

2 Answers2

8

Overview

Since 'leaflet' maps are interactive, the leaflet object being used in mapview::mapshot() function must also be interactive. Accounting for this allows for the user to save their version of a leaflet map within the Shiny app.

# install necessary packages
install.packages( c( "shiny", "leaflet", "mapview" ) )

# load necessary packages
library( shiny )
library( leaflet )
library( mapview )

ui <- fluidPage(
  leafletOutput( outputId = "map"),
  downloadButton( outputId = "dl")
)

server <- function(input, output, session) {
  
  # Create foundational leaflet map
  # and store it as a reactive expression
  foundational.map <- reactive({
    
    leaflet() %>% # create a leaflet map widget
      
      addTiles( urlTemplate = "https://{s}.tile.openstreetmap.se/hydda/base/{z}/{x}/{y}.png" ) # specify provider tile and type
    
  }) # end of foundational.map()
  
  # render foundational leaflet map
  output$map <- leaflet::renderLeaflet({
    
    # call reactive map
    foundational.map()
    
  }) # end of render leaflet
  
  # store the current user-created version
  # of the Leaflet map for download in 
  # a reactive expression
  user.created.map <- reactive({
    
    # call the foundational Leaflet map
    foundational.map() %>%
      
      # store the view based on UI
      setView( lng = input$map_center$lng
               ,  lat = input$map_center$lat
               , zoom = input$map_zoom
      )
    
  }) # end of creating user.created.map()
  
  
  
  # create the output file name
  # and specify how the download button will take
  # a screenshot - using the mapview::mapshot() function
  # and save as a PDF
  output$dl <- downloadHandler(
    filename = paste0( Sys.Date()
                       , "_customLeafletmap"
                       , ".pdf"
    )
    
    , content = function(file) {
      mapshot( x = user.created.map()
               , file = file
               , cliprect = "viewport" # the clipping rectangle matches the height & width from the viewing port
               , selfcontained = FALSE # when this was not specified, the function for produced a PDF of two pages: one of the leaflet map, the other a blank page.
      )
    } # end of content() function
  ) # end of downloadHandler() function
  
} # end of server

# run the Shiny app
shinyApp(ui = ui, server = server)

# end of script #

Final result

Once you run the Shiny app, open the app in a new window.

RStudio View

Once in the browser, go ahead and click Download. It took about ~3 seconds.

Chrome View

Once Download has been clicked, you'll promptly see a PDF file wherever your downloaded files are stored on your machine.

Final output

References

My ideas sprung from the following posts:

Cristian E. Nuno
  • 2,822
  • 2
  • 19
  • 33
  • 1
    This is great! Very helpful. You can also get the map center with input$map_center$lng and input$map_center$lat. So you can just use setView(lng = input$map_center$lng, lat = input$map_center$lat, zoom = input$map_zoom) – Hallie Swan Mar 26 '18 at 02:25
  • @blondeclover thank you! This makes the code cleaner and easier to understand. I've updated the answer to reflect this new knowledge. Now if only there was a place where I could read more about leaflet input events - they are very handy for instances such as this! – Cristian E. Nuno Mar 26 '18 at 03:47
  • 3
    definitely! I usually combine `renderPrint({reactiveValuesToList(input)})` on the server and `verbatimTextOutput` on the ui to see all the inputs available. That's where I saw the `input$map_center` option – Hallie Swan Mar 26 '18 at 04:06
  • 1
    @blondeclover - NOW THIS IS AMAZING! haha this is exactly what I wanted: a list of all available inputs. Thank you for showing this to me. I've added a screenshot and the code necessary to print out these input events. I hope others find this useful in the development of their leaflet maps within Shiny apps! – Cristian E. Nuno Mar 26 '18 at 04:32
  • 1
    I want to save a leaflet map with the drawn shapes and lines drawn via addDrawToolbar in the leaflet.extras package. I have made use of your answer but it did not work. Could you also have a look at it? https://stackoverflow.com/questions/53650039/how-to-save-a-leaflet-map-with-drawn-shapes-points-on-it-in-shiny. – BRCN Dec 06 '18 at 11:24
  • I've moved the issue to [`mapview`](https://github.com/r-spatial/mapview/issues/198) so hopefully someone will help out soon! – Cristian E. Nuno Dec 14 '18 at 02:51
  • thanks for the solution! However when I publish the app the download returns empty HTML, and not pdf. Any idea why? – mpetric Apr 26 '21 at 11:44
  • 1
    @mpetric - no problem! As far your situation goes, I am not sure why when you publish the Shiny app, the download button returns blank HTML and not a PDF file. My recommendation is that you make a reproducible example, link to this solution, and ask a new question. As this post nearly turns 3 year old, it might be good for the R community to have a newer question posted. – Cristian E. Nuno Apr 26 '21 at 18:36
  • 1
    @Christian E. Nuno - solved it by including: webshot::install_phantomjs(force = TRUE) in the preamble – mpetric Apr 27 '21 at 20:08
5

May be this would help:

  server <- function(input, output, session) {

    map <- reactiveValues(dat = 0)

      output$map <- renderLeaflet({
        map$dat <- leaflet() %>% 
          addTiles()
      })

      output$dl <- downloadHandler(
        filename = "map.png",

        content = function(file) {
          mapshot(map$dat, file = file)
        }
      )
    }
SBista
  • 7,479
  • 1
  • 27
  • 58