0

I'm trying to create a shiny app that will allow users to click on a point in the map, show where they clicked with a marker, allow them to label the point with a name (that they type in free text) and then save the latitude, longitude and label/name of all the points they added to a data.frame which can be exported as a .csv with a download button.

I've worked out how to record the labels, with textInput() and submission of the label when they press the enter key (using java code I copied from another S/O post). Then they click on the place in the map that they want to add the marker, the markers are added with leafletProxy() and the last label they added is automatically assigned - which is a bit clunky, because they have to delete the text in the box to type in the next label, but I will try to figure out how to sort that later.

My main problem is that I can't figure out how to extract the latitude, longitude and assigned marker label to a data.frame(). For this, I need to understand the structure of input$map_click and I can't seem to find that anywhere - I know it is a series of lists but not where the three things I want to extract are. Ideally I would also like to also display the table in the Shiny app underneath the map and have it update with a new row every time a point was added.

More generally I'm still a novice when it comes to Shiny, and I would like to understand how I can view the structure of something created in the app based on a user interaction, in my r console during development (for example in this case how to save a copy of input$map_click in my r environment so that I can inspect it).

Here is some example code:

# Load libraries -------------------------------------------------------

# Check if pacman is installed, install if not:
if (!require("pacman")) install.packages("pacman")

# Use pacman::p_load to load required packages:
pacman::p_load(shiny, 
               DT,
               here, 
               rio, 
               dplyr,
               sf,
               leaflet)

# Create UI ------------------------------------------------------------

ui <- fluidPage(
  
  # Create a title for the page:
  title = "Localisation des nouveau quartiers" ,
  
  # Place title in the centre:
  position = "static-top",
  
  # Add extra javascript to record when enter key is pressed:
  tags$script('
  $(document).on("keyup", function(e) {
  if(e.keyCode == 13){
    Shiny.onInputChange("keyPressed", Math.random());
    }
  });
  '),

  # Set output as leaflet map:
  leafletOutput("map"),
  
  # Create text box for GPS point label:
  textInput(inputId = "nom_du_point",
            label = "Proposer un nom pour votre nouveau quartier",
            value = "",
            width = "100%"), 
  
  # Add table of coordinates:
  DTOutput(outputId = "quartier_centroids")
  
)

server <- function(input, output, session) {
  
  # Render the leaflet map:
  output$map <- renderLeaflet(
    
    leaflet() %>% 
      # Add open street map (default base layer)
      addTiles(options = tileOptions(maxZoom = 18))
     
  )
  
  
  # Record label entered by user:
  gpslabel <- reactiveVal()
  
  observeEvent(input[["keyPressed"]], {
    
    gpslabel(input[["nom_du_point"]])
    
    # Clear the text input to allow for a new label:
    updateTextInput(session, 
                    inputId = "service", 
                    value = "")
    
  })
  

  # Add new markers to map by user clicking:
  observeEvent(input$map_click, {
    
    # Add coordinates on click:
    click <- input$map_click

    # Add marker and label to the map using coordinates and text entry:
    leafletProxy('map') %>% 
      addMarkers(lng = click$lng, 
                 lat = click$lat, 
                 label = gpslabel()) 
    
    # Add table showing results under map:
    output$quartier_centroids <- renderDT({
      
      DT::datatable(click, editable = TRUE)
      
    })
    
    
  })
  
}

# Run the application 
shinyApp(ui = ui, server = server)


Note that this code allows me to add a label/name for a point, then drop a pin on the map which automatically has the last label assigned to it. But the table is not showing at the bottom, and gives me this error:

Error: data must be 2-dimensional (e.g. data frame or matrix)

After registering a label and a marker on the map, in my R console I get the following details:

List of 3
 $ lat   : num 19.1
 $ lng   : num -72.3
 $ .nonce: num 0.133
Warning: Error in DT::datatable: 'data' must be 2-dimensional (e.g. data frame or matrix)
  108: stop
  107: DT::datatable
  106: exprFunc [#58]
  105: widgetFunc
  104: ::
htmlwidgets
shinyRenderWidget
  103: func
   90: renderFunc
   89: renderFunc
   85: renderFunc
   84: output$quartier_centroids
    3: runApp
    2: print.shiny.appobj
    1: <Anonymous>

I'm assuming this is because click is a list and I don't know which parts of the list to access to get the latitude, longitude and label.

Edit:

In the above code I tried first of all just to put the click object in a table, but what I really want to do is put the marker details in a table because that will include the assigned label as well - and this is what I don't know how to do.

1 Answer 1

1

Save longitudes and latitudes in a downloadable table

Mark clicked longitudes and latitudes

You actually got that part right. Your app correctly creates a marker at the clicked longitudes and latitudes, but the marker icon does not show up because of an issue with leaflet.js.

As suggested here, you can rely on the function addAwesomeMarkers and specify the marker of your choice to fix it.

Store click info in a downloadable table

The key missing part here is that your table must be reactive. There are several ways to achieve that. A simple one is to use reactiveVal, as you did for gpslabel.

Then you can generate a download button for the table with downloadHandler.

library(DT)
library(shiny)
library(leaflet)

icon_url <- "https://raw.githubusercontent.com/rstudio/leaflet/main/docs/libs/leaflet/images/marker-icon.png"
tab <- na.omit(data.frame(longitude = NA_real_, latitude = NA_real_))

ui <- fluidPage(leafletOutput("map"), 
                DTOutput("tab"),
                downloadButton("download", "Download csv"))

server <- function(input, output, session) {
  
  tab <- reactiveVal(tab)
  
  output$map <- renderLeaflet(leaflet() %>% 
                                setView(-1.679272, 48.111368 , 12) %>%
                                addTiles(options = tileOptions(maxZoom = 18)))
  
  observeEvent(input$map_click, {
    
    leafletProxy('map') %>% 
      addAwesomeMarkers(lng = input$map_click$lng, 
                        lat = input$map_click$lat, 
                        icon = makeAwesomeIcon(icon_url))
    
    tab(rbind(tab(), data.frame(longitude = input$map_click$lng, 
                                latitude = input$map_click$lat)))
    
  })
  
  output$tab <- renderDT({datatable(tab())})
  
  output$download <- downloadHandler(
    filename = "quartiers.csv",
    content = function(file) {write.csv(as.data.frame(tab()), file, row.names = F)}
  )
}

shinyApp(ui = ui, server = server)

enter image description here

Enter labels as free text

As you acknowledged, how the user enters the labels here is not very practical. The main problem with this approach is that the text input is independent from the click. I suggest to force each label to be associated to a given click by making the text input widget appear in a popup window.

The package shinyalert allows to implement that easily:

library(DT)
library(shiny)
library(leaflet)
library(shinyalert)

icon_url <- "https://raw.githubusercontent.com/rstudio/leaflet/main/docs/libs/leaflet/images/marker-icon.png"
tab <- na.omit(data.frame(longitude = NA_real_, latitude = NA_real_, label = NA_character_))

ui <- fluidPage(useShinyalert(),
                leafletOutput("map"), 
                DTOutput("tab"),
                downloadButton("download", "Download csv"))

server <- function(input, output, session) {
  
  tab <- reactiveVal(tab)
  
  output$map <- renderLeaflet(leaflet() %>% 
                                setView(-1.679272, 48.111368 , 12) %>%
                                addTiles(options = tileOptions(maxZoom = 18)))
  
  observeEvent(input$map_click, {
    
    shinyalert(html = TRUE, showConfirmButton = F,
               text = tagList(HTML(paste0("Proposition :<br>")), 
                              textInput("prop", ""),
                              actionButton("enter", "Valider")))
  })
  
  observeEvent(input$enter, {
    
    leafletProxy('map') %>% 
      addAwesomeMarkers(lng = input$map_click$lng, 
                        lat = input$map_click$lat, 
                        icon = makeAwesomeIcon(icon_url),
                        label = input$prop)
    
    tab(rbind(tab(), data.frame(longitude = input$map_click$lng, 
                                latitude = input$map_click$lat,
                                label = input$prop)))
  })
  
  output$tab <- renderDT({datatable(tab())})
  
  output$download <- downloadHandler(
    filename = "quartiers.csv",
    content = function(file) {write.csv(as.data.frame(tab()), file, row.names = F)}
  )
}

shinyApp(ui = ui, server = server)

enter image description here

2
  • I am getting a warning/error when I try to click on the map which is forcing the app to exit: Warning: Error in &&: 'length = 3' in coercion to 'logical(1)' 3: runApp 2: print.shiny.appobj 1: <Anonymous>
    – Amy M
    Commented Jun 8, 2023 at 12:22
  • Note this is with R 4.3.0 running the exact same code as you provided above. I can see this type of error was identified for some other packages in 2022 but don't really understand what it is or how to fix it. I would be grateful for any insights?
    – Amy M
    Commented Jun 8, 2023 at 12:25

Not the answer you're looking for? Browse other questions tagged or ask your own question.