Search code examples
rshinyr-leaflet

How to reset a Leaflet click event in Shiny


I'm building a Leaflet app in Shiny, and the idea is that the user logs in to see an overview of specific cities that can be clicked on. Clicking on those cities will zoom in to that particular city. The code as shown below works in making this happen.

What I can't figure out, is that I also want to provide a button (that appears after a city is selected) called "Return to see other cities" that, when clicked, will "reset" the map back to its original state.

However, when I click on the button, I get the following error:

Warning: Error in UseMethod: no applicable method for 'metaData' applied to an object of class "NULL"

Here I'm using input$map_marker_click to determine which city the app should zoom in on, and I think a part of the problem is that I can't set that back to its initial state of NULL after it's been clicked. Is there another way I'm missing?

library(shiny)
library(leaflet)
library(leaflet.extras)
library(dplyr)

cities <- data.frame(cities = c("London", "Chicago", "New York", "Philadelphia", "Los Angeles"),
                 lng = c(-0.118092, -87.6298, -74.0060, -75.1642, -118.2477),
                 lat = c(51.509865, 41.848, 40.7128, 39.9586, 34.0522),
                 zoom = c(11, 11, 12, 12, 10))

ui <- bootstrapPage(
  tags$style(type = "text/css", "html, body {width:100%;height:100%}"),
  leafletOutput("map", width = "100%", height = "100%"),
  conditionalPanel("isNaN(input.map_marker_click)", uiOutput("controls"))
)

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

  output$map <- renderLeaflet({
    leaflet(cities, width = "100%", height = "100%") %>%
      addProviderTiles("CartoDB.DarkMatter") %>%
      setView(lng = -56, lat = 49.2402, zoom = 4) %>% 
      addPulseMarkers(lng = ~lng, lat = ~lat,
                      label = ~cities,
                      icon = makePulseIcon())
  })

  output$controls <- renderUI({
    req(input$map_marker_click)
    absolutePanel(id = "controls", top = 100, left = 50, 
                    right = "auto", bottom = "auto", width = "auto", height = "auto",
                    actionButton(inputId = "reset", label = "Return to see other cities", class = "btn-primary")
    )
  })

  observeEvent(input$map_marker_click, {
    city_selected <- filter(cities, lat == input$map_marker_click$lat[1])

    leafletProxy("map") %>%
      clearMarkers() %>%
      clearControls() %>%
      setView(lng = city_selected$lng[1], lat = city_selected$lat[1], zoom = city_selected$zoom[1])
  })

#I know the below is wrong, but I don't know what I'm supposed to do to "reset" the map.
  observeEvent(input$reset, {

    leafletProxy("map") %>%
      setView(lng = -56, lat = 49.2402, zoom = 4) %>%
      addPulseMarkers(lng = ~lng, lat = ~lat,
                      label = ~cities,
                      icon = makePulseIcon())
  })
}

shinyApp(ui, server)

Solution

  • An alternative reactive based output

    Enclosing map creation inside a function so that resetting becomes easier by just a function call without code repetition:

    library(shiny)
    library(leaflet)
    library(leaflet.extras)
    library(dplyr)
    library(shinyjs) #for hide function
    
    cities <- data.frame(cities = c("London", "Chicago", "New York", "Philadelphia", "Los Angeles"),
                         lng = c(-0.118092, -87.6298, -74.0060, -75.1642, -118.2477),
                         lat = c(51.509865, 41.848, 40.7128, 39.9586, 34.0522),
                         zoom = c(11, 11, 12, 12, 10))
    
    ui <- bootstrapPage(
      useShinyjs(),
      tags$style(type = "text/css", "html, body {width:100%;height:100%}"),
      leafletOutput("map", width = "100%", height = "100%"),
      conditionalPanel("isNaN(input.map_marker_click)", uiOutput("controls"))
    )
    
    server <- function(input, output, session) {
    
      #creating the first map within a function so that reset becomes easy
    
      base_map <- function(){
        leaflet(cities, width = "100%", height = "100%") %>%
          addProviderTiles("CartoDB.DarkMatter") %>%
          setView(lng = -56, lat = 49.2402, zoom = 4) %>% 
          addPulseMarkers(lng = ~lng, lat = ~lat,
                          label = ~cities,
                          icon = makePulseIcon())
      }
    
      # reactiveVal for the map object, and corresponding output object.
      react_map <- reactiveVal(base_map())
      output$map <- renderLeaflet({
        react_map()
      }) 
    
      output$controls <- renderUI({
        req(input$map_marker_click)
        absolutePanel(id = "controls", top = 100, left = 50, 
                      right = "auto", bottom = "auto", width = "auto", height = "auto",
                      actionButton(inputId = "reset", label = "Return to see other cities", class = "btn-primary")
        )
      })
    
      observeEvent(input$map_marker_click, {
        city_selected <- filter(cities, lat == input$map_marker_click$lat[1])
    
        show('controls')
    
        leafletProxy("map") %>%
          clearMarkers() %>%
          clearControls() %>%
          setView(lng = city_selected$lng[1], lat = city_selected$lat[1], zoom = city_selected$zoom[1])
      })
    
      # Making the entire map creation inside reactive function makes it easier to reset
      observeEvent(input$reset, {
    
        #hiding the control button
        hide('controls')
    
        # resetting the map
    
        react_map(base_map()) 
    
      })
    }
    
    shinyApp(ui, server)