Trouble with validation

I'm having trouble validating user input into a Shiny app.
Basically, if the user enters a correctly-formatted NWIS site number (no trailing or leading zeros) which exists in the NWIS database, the shiny app works fine. However, if the NWIS site number has trailing zeros, or if the site number does not exist as identified using:

site_data <- whatNWISsites(siteNumber=SITE_NUM, parameterCd=paraCode)

then the app crashes.
Where in the app do I add a validation call to check if whatNWISsites returns a site, and not crash the app if it doesn't? Right now I'm toying around with a validate call within an observeEvent function, but it doesn't seem to be the correct organization.

##############################################################################
# Libraries
##############################################################################
rm(list=ls())
list.of.packages <- c("RColorBrewer","dataRetrieval",
                      "curl","repr","maps","dplyr",
                      "ggplot2","leaflet","leafem","raster",
                      "raster","shiny","htmlwidgets","devtools",
                      "shinycustomloader","shinydashboard","shinyjs","DT",
                      "spData","sf","shinythemes","plotly","tryCatchLog")
new.packages <- list.of.packages[!(list.of.packages %in% installed.packages()[,"Package"])]
if(length(new.packages)) install.packages(new.packages)
lapply(list.of.packages, require, character.only = TRUE)

##############################################################################
# UI Side
##############################################################################
ui <- fluidPage(
  shinyjs::useShinyjs(),
  h1(id="big-heading", "USGS Gages Annual Flow Peak Tool"),
  tags$style(HTML("
      @import url('//fonts.googleapis.com/css?family=Lobster|Cabin:400,700');
      
      h1 {
        font-family: 'Lobster', cursive;
        font-weight: 500;
        line-height: 1.1;
        color: #006F41;
      }

    ")),
  
  # side panel
  sidebarPanel(

    textInput(inputId ="site_no", 
              label = "Site Number", 
              width = '400px',
              value="01615000",
              placeholder = "Please enter the NWIS Site Number."),
    textInput(inputId ="years_of_records", 
              label = "Years of Records", 
              width = '400px',
              value = 30,
              placeholder = "How many years of Records would you like?"),
    textInput(inputId ="da_epsilon", 
              label = "Drainage Area Epsilon", 
              width = '400px',
              value = 0.25,
              placeholder = "What is the Drainage Area Epsilon?"),
    textInput(inputId ="bbox_delta", 
              label = "Bounding Box Delta - Degrees", 
              width = '400px',
              value = 1,
              placeholder = "What is the Bounding Box delta?"),
    
    actionButton(
      inputId = "SubmitButton",
      label = "Submit"
    ),
    
    downloadButton('downloadData', 'Download Data'),
    h4(''),
    dataTableOutput('table01'),
    width = 3),
  
  # main panel
  mainPanel(
    leafletOutput('map01', width = "110%", height="500px"),
    br(),
    plotlyOutput('hist01', width = "110%")
  )
)

##############################################################################
# Server Side
##############################################################################
server <- function(input,output){
    
    dataInput <- reactive({
      SITE_NUM <- input$site_no
      SITE_URL <- paste0("https://waterdata.usgs.gov/nwis/inventory/?site_no=",SITE_NUM,"&agency_cd=USGS")
      paraCode <- "00060"
      years_of_records <- as.numeric(input$years_of_records)
      da_epsilon <- as.numeric(input$da_epsilon)
      bbox_delta <- as.numeric(input$bbox_delta) # Degrees
      
      # CODE TO MAKE DATA FRAME
      ##-------------------------------------------------------------------##
      ## CHECK TO SEE IF THIS doesnt work ##
      site_data <- whatNWISsites(siteNumber=SITE_NUM, parameterCd=paraCode)
      ## IF IT DOESnt work, alert the user and dont crash the app please
      ##-------------------------------------------------------------------##
      
      site_lat <- site_data$dec_lat_va
      site_long <- site_data$dec_long_va
      site_data$site_url <- SITE_URL
      
      # Get site drainage area
      site_summary <- readNWISsite(siteNumber=SITE_NUM)
      site_da <- site_summary$drain_area_va
      
      # need to use SIG FIGS --- Otherwise the curl command gets confused.
      bBox <- c(signif(site_long - bbox_delta,7),
                signif(site_lat - bbox_delta,7),
                signif(site_long + bbox_delta,7),
                signif(site_lat + bbox_delta,7))
      
      bbox_shiny <- c(bBox[1],bBox[3],bBox[2],bBox[4])
      
      # Get site metadata for the Bbox
      para_sites <- as.data.frame(whatNWISsites(bBox=bBox, parameterCd=paraCode))
      para_sites$gtype = paraCode #gtype: gage type (stage, flow, ...etc)
      
      # Filter the retrieved USGS gages based on the defined criteria
      sites_meta <- whatNWISdata(siteNumber=para_sites$site_no, parameterCd=paraCode)
      sites_meta_years <- sites_meta[(sites_meta['end_date'] - sites_meta['begin_date']) > (years_of_records * 365.0),]
      sites_summary <- readNWISsite(siteNumber=sites_meta_years$site_no)
      sites_selected <- sites_summary[((1-da_epsilon)* site_da) <= sites_summary['drain_area_va'] & sites_summary['drain_area_va'] <= ((1+da_epsilon)* site_da), ]
      # Separate surrounding sites
      site_surrounding <- sites_selected[sites_selected$site_no != SITE_NUM, ]
      
      # Append URL 
      for(i in 1:nrow(sites_selected)){
        sites_selected_no <- as.character(sites_selected$site_no)
        sites_selected$site_url <- paste0("https://waterdata.usgs.gov/nwis/inventory/?site_no=",sites_selected_no,"&agency_cd=USGS")
      }
      
      # Separate central site
      red_site <- sites_selected[sites_selected$site_no == paste(SITE_NUM),]
      
      # GET PEAK STREAMFLOW DATA
      # Select columns
      peak_ts <- readNWISpeak(input$site_no)
      cols = c("site_no","peak_dt","peak_va","gage_ht")
      peak_ts <- cbind(red_site[,"station_nm"], peak_ts[,cols])
      # Change names
      names(peak_ts) <- c("Station Name", "Site Number", "Peak Streamflow: Date", "Peak streamflow (cfs)", "Gage Height (feet)")
      chart_title=paste(peak_ts[1,1], peak_ts[1,2],': Peak streamflow (cfs)')
      
      
      output$table01 <- renderDataTable({
        DT::datatable(peak_ts%>% select(-"Station Name"), 
                      selection = "single",
                      extensions = 'Responsive',
                      rownames=FALSE,
                      options=list(stateSave = FALSE, 
                                   autoWidth = TRUE,
                                   lengthMenu = c(5, 10, 20)))})
      
      output$map01 <- renderLeaflet({
        
        leaflet(sites_selected) %>% 
          clearShapes() %>%
          addTiles() %>% 
          leafem::addMouseCoordinates() %>% 
          leafem::addHomeButton(extent(us_states),"Zoom to Home")%>%
          fitBounds(~min(dec_long_va), ~min(dec_lat_va), ~max(dec_long_va), ~max(dec_lat_va)) %>% 
          addCircleMarkers(data = red_site,
                           lng= ~dec_long_va,
                           lat = ~dec_lat_va,
                           color='red',
                           popup= paste0( red_site$station_nm,
                                          "<br>", "USGS site: ", red_site$site_no,
                                          "<br>", "<a href='", red_site$site_url,
                                          "' target='_blank'>", "USGS URL</a>"),
                           label = red_site$station_nm) %>% 
          addCircleMarkers(data = site_surrounding,
                           lng= ~dec_long_va,
                           lat = ~dec_lat_va,
                           color='blue',
                           popup= paste0( site_surrounding$station_nm,
                                          "<br>", "USGS site: ", site_surrounding$site_no,
                                          "<br>", "<a href='", site_surrounding$site_url,
                                          "' target='_blank'>", "USGS URL</a>"),
                           label = site_surrounding$station_nm)
      })
      
      output$hist01 <- renderPlotly({

        ggplot() +
          geom_bar(aes(x=peak_ts[,"Peak Streamflow: Date"],y=peak_ts[,"Peak streamflow (cfs)"]),
                   stat="identity", 
                   width=125) +
          ylab('Peak streamflow (cfs)') +
          xlab('Date') +
          # xlim(min(qDat$drain_area_va), max(qDat$drain_area_va))+
          ggtitle(chart_title)+
          theme(text = element_text(family = "Arial", color = "grey20", size=12, face="bold"))
        
      })
    })
    
    
  observeEvent(input$SubmitButton, {
    print("Submission received")
    print("validating")
    validate(need(dataInput(),"Dataframe not found")) #
    dataInput()
    
  })
    
  
  output$map01 <- renderLeaflet({
    
    leaflet() %>% setView(-93.65, 42.0285, zoom = 4) %>% addTiles()
  })
}

shinyApp(ui, server)

Hi,

Here is a small example of a solution you could implement:

library(shiny)
library(dataRetrieval)
library(stringr)

ui <- fluidPage(
  textInput("site_no", "Site Number"),
  actionButton("getInfo", "Get site info"),
  tableOutput("siteData")
)

server <- function(input, output, session) {
  
  siteData = reactiveVal()
  
  observeEvent(input$getInfo, {
    
    #Show busy message during search
    showModal(modalDialog(title = "BUSY", HTML("<h2>Looking for website
      <img src = 'https://media.giphy.com/media/sSgvbe1m3n93G/giphy.gif' height='50px'></h2>"), footer = NULL))
    
    #Check if site exists
    error = "none"
    siteData = tryCatch(whatNWISsites(siteNumber = input$site_no, parameterCd = "00060") , 
                   error = function(x) error <<- x)
    
    #Based on results display message or data
    if(error != "none"){
      showModal(modalDialog(title = "SITE NAME ERROR", str_extract(error, "(?<=: ).*")))
      siteData()
    } else {
      removeModal()
      siteData(siteData)
    }
    
  })
  
  output$siteData = renderTable({
    siteData()
  })
  
}

shinyApp(ui, server)

The answer to your question is the tryCatch function, which will capture an error or message from the output. I captured any error (and cleaned it a bit) if present. When present, the user will see the error message, if not, the data will be displayed.

Try it with the following values to see different messages: <empty>, 01615000, 0161500000, 01615x000

Because the retrieval of data can take a few seconds, especially in case of error when the function tries several times. I added an optional waiting message. You have different ways of implementing this, but I just went with a modal approach since any error will be a modal too.

Hope this helps,
PJ

1 Like

Wow - thank you so much PJ. Not only did you demo the validation, you gave an example of a loading gif too! Can't thank you enough, I've been banging my head against the desk trying to solve both issues! Cheers!

1 Like

You're welcome :slight_smile:

By the way, it bothered me that I could not use a local image (gif) for the modal because it was not displaying. Turns out local images in modals only get loaded once background processes have finished (which is not the case for images from the web). There is a workaround though. You have to replace the source with this:

tags$img(src = base64enc::dataURI(file = "www/myImage.jpg", mime = "image/jpeg"))

More on this can be read here:

Again, modals are not the ideal way for progress messages, so I recommend you use something else if you want to expand on it.

PJ

1 Like

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.