## Attach packages
library(shiny)
library(caladaptr)
library(ggplot2)
library(shinyhelper)
library(leaflet) ## used a lot in app.R
library(dplyr)
library(shinyjs) ## must use library; requirenamespace won't cut it
## The following is an attempt to 'trick' the Shiny app publisher to
## make sure the following packages are installed on the server
## They are not actually needed right here
## This works I should add scales and DT here also.
#######################################################################
## Check for required namespaces
## Instead of using library(), the following packages are verified as available
## but not attached. I do this as general best practice to minimize name clashes,
## in three cases:
## 1) I am only using one or two functions from the package and will invoke them
## explicitly (e.g., scales::percent).
## 2) In the case of DT, I choose to explicitly call those functions
## because there are identical functions in shiny which I explicitly don't want.
## 3) The functions are not needed here but needed in report.Rmd
requireNamespace("DT")
requireNamespace("lubridate") ## only 1-2 functions used in app.R
requireNamespace("htmltools") ## basically all of the functions needed in app.R are re-exported from Shiny. Need for Rmd
requireNamespace("conflicted") ## one function used in app.R (also Rmd)
requireNamespace("units") ## only one function used in app.R
requireNamespace("stringr") ## use 2 functions in app.R (str_wrap and str_split)
requireNamespace("rmarkdown") ## used to launch report.Rmd
requireNamespace("kableExtra") ## used in report.Rmd
requireNamespace("tidyr") ## pivot_* functions used here and report.Rmd
requireNamespace("httr")
## requireNamespace("scales") No longer needed - I format columns as percent using DT or manually with paste0()
## requireNamespace("chillR") chillR is problematic to install on ShinyApps.io
## Specifically, the dependent package RMAWGEN has 3 dependencies that
## aren't picked up by rsconnect.
## The following three packages are *not* needed, but are dependencies of
## RMAWGEN. For some reason however the script which deploys this app
## to ShinyApps.io doesn't automatically install these dependencies,
## so to get the app to deploy I have to pretend like this script needs them.
# MEMORY HIT: THESE THREE NAMESPACES CHEW 10 MB
# UPDATE: NO LONGER NEEDED B/C I NO LONGER LOAD CHILLR DIRECTLY
# requireNamespace("date")
# requireNamespace("vars")
# requireNamespace("chron")
## Set up conflict resolution
conflicted::conflict_prefer("filter", "dplyr", quiet = TRUE)
conflicted::conflict_prefer("count", "dplyr", quiet = TRUE)
conflicted::conflict_prefer("select", "dplyr", quiet = TRUE)
conflicted::conflict_prefer("show", "shinyjs", quiet = TRUE)
## Utility function to add an additional class to the outer-most div of a shiny-tag
shinytag_add_class <- function(x, additional_class) {
if (!inherits(x, "shiny.tag")) stop("x should be of class shiny.tag")
x$attribs$class <- paste(x$attribs$class, additional_class)
x
}
## Utility function to modify a Shiny tag with 2 children, wrapping the 2nd child in a DIV with a style attribute.
## If style = "display:inline-block;", this will have the effect of making the label inline.
shinytag_wrapchild2div <- function(x, style) {
if (length(x$children) != 2) stop("This function is designed to modify a shiny.tag with two children")
x$children[[2]] <- div(x$children[[2]], style = style)
x
}
# define js function for opening urls in new tab/window
# js_code <- "shinyjs.browseMe = function(url) {
# window.open(url,'_blank');
# }"
## Utility function to print memory usage to the console
# requireNamespace("pryr")
# report_memory <- function(x) {
# cat("\n", x, "\n", sep = "")
# cat(" - total memory used: ", round(as.numeric(pryr::mem_used()) / 1000000), " MB\n", sep = "")
# cat(" - objects in memory: ", paste(ls(), collapse = ", "), "\n", sep = "")
# cat(" - memory used by objects is: ", as.numeric(pryr::object_size(ls())) / 1000000, " MB\n", sep = "")
# }
## Load a custom version of chillr::make_hourly_temps
source('ca_make_hourly_temps.R')
source('chillr_daylength.R')
source('chillr_dynamic_model.R')
## Setup development logging to capture the frequency of API connection errors which is an issue on
## ShinyApps.io. (will only work if log_setup.R is present)
log_fn <- "log_setup.R"
log_yn <- file.exists(log_fn)
cat("log_yn =", log_yn, "\n")
if (log_yn) source(log_fn)
gtag_fn <- "gtag_chill2.js"
# ## Do a quick check upon initialization if we have a connection with the Cal-Adapt
# endpts_resp <- try(httr::GET(url = ca_baseurl))
# if (class(endpts_resp)[1] == "try-error") {
# if (log_yn) {
# log_entry_df <- data.frame(dt_utc = Sys.time(),
# host = "unavailable",
# path = "hard to say",
# port = "no idea",
# shiny_port = "",
# event = "Could NOT connect to Cal-Adapt API at all")
# googlesheets4::sheet_append(ss = log_ss_id, data = log_entry_df, sheet = log_sheet)
# }
#
# } else {
# ## No error
# if (log_yn) {
# log_entry_df <- data.frame(dt_utc = Sys.time(), host = "",
# path = "", port = "",
# shiny_port = "", event = "Connected to Cal-Adapt API!!")
# googlesheets4::sheet_append(ss = log_ss_id, data = log_entry_df, sheet = log_sheet)
# }
# }
## report_memory("Memory usages after packages are loaded")
ui <- function(request) {
fluidPage(
## Only load the Google Analytics tracking tag if the app is running on ShinyApps.io
if (file.exists(gtag_fn) && Sys.getenv('SHINY_PORT') != "") {
tags$head(includeHTML(gtag_fn))
},
# set up shiny js to be able to call our browseMe function
useShinyjs(),
#extendShinyjs(text = js_code, functions = 'browseMe'),
tags$head(tags$style(type="text/css",
"p.step {color:black; font-weight:bold; font-size:120%; padding-top:5px;}
p.topborder {border-top:3px solid lightgray;}
p.desc {font-style:italic; font-size:90%;}
h1.report {font-weight:bold; font-size:16px; padding-top:10px; border-top:1px solid lightgray;}
h2 {font-weight:bold;}
h3 {font-weight:bold;}
.leaflet-container {cursor: pointer !important;}
.iblock {display: inline-block;} /* inline block for side-by-side UI elements */
div.error-msg {color:red; margin:1em 0;}
div.happy-msg {color:green; margin:1em 0;}
div#shiny-notification-panel {right:0; left:0; margin:0 auto;}
.space_above_below {margin-top:0.5em; margin-bottom:0.5em;}
.indented2 {margin-left:2em;}")
),
## We put this tag in the body of the HTML document, rather than the head, so it doesn't
## get over-written by shinyhelper.css. This is needed to move the shinyhelp info buttons
## closer to the label rather than the far right edge of the column
tags$style(type="text/css",
".shinyhelper-container {display: inline-block; position: relative; margin-left: 1em;}
div.dt-buttons {margin-top:5px; float:right;}"),
titlePanel(title = "Projected Chill Portions Under Climate Change Calculator 2.0",
windowTitle = "Chill Portions Calculator"),
fluidRow(
column(12,
textOutput("txtout_connect_status") %>% shinytag_add_class("error-msg"),
tags$p(),
tags$div(tags$a(
href = "https://cran.r-project.org/package=chillR",
tags$img(src="chillr_hex_75x65.png", height = "75px"),
target = "_blank",
rel = "noopener"),
style = "margin-top:10px; float:right; padding-left:10px; width:65px;"),
tags$div(
tags$a(href = "https://ucanr-igis.github.io/caladaptr/",
tags$img(src="caladaptr_hex_173x200.gif",
height = "75px"),
target = "_blank",
rel = "noopener"),
style = "margin-top:10px; float:right; width:65px;"),
tags$p("Introduction", class = "step topborder"),
HTML("<p>This calculator can be used to compute end-of-season <a href='http://fruitsandnuts.ucdavis.edu/Weather_Services/chilling_accumulation_models/about_chilling_units/' target='_blank' rel='noopener'>chill portions</a> under projected climate scenarios. The calculator draws upon downscale projected climate data from the California 4th Climate Change assessment hosted on <a href='https://cal-adapt.org/' target='_blank' rel='noopener'>Cal-Adapt.org</a>.</p>"),
HTML("<details>
<summary style='color:#337ab7; cursor:pointer; outline:none;'>Click here for more info.</summary>
<ul>
<li>This is a pilot app for demonstration purposes only. A <a href='https://youtu.be/5TYs3dbUU7A' rel='noopener' target='_blank'>YouTube demo</a> is available.</li>
<li>This calculator uses downscaled climate data from Cal-Adapt, which is available for California, Nevada, and a <a href='https://ucanr-igis.github.io/caladaptr-res/workshops/caladaptr_intro_dec20/slides_files/figure-slidy/unnamed-chunk-6-1.png' target='_blank'>little bit of neighboring states</a> in the western USA.</li>
<li>This calculator uses chill <i>portions</i> rather than chill <i>hours</i>, because chill portions do a better job at predicting tree phenology. <a href='http://fruitsandnuts.ucdavis.edu/Weather_Services/chilling_accumulation_models/about_chilling_units/' target='_blank' rel='noopener'>More info</a>.</li>
<li>RStudio users can run this and other demo Shiny apps directly from RStudio
using the <a href=\"https://github.com/ucanr-igis/caladaptr.apps\" target=\"_blank\" rel=\"noopener\">caladaptr.apps</a> package.</li>
<li>To compute projected chill portions using a script, see this <a href='https://ucanr-igis.github.io/caladaptr-res/notebooks/chill.nb.html' target='_blank' rel='noopener'>
R Notebook</a>.</li>
<li>If the calculator unexpectedly disconnects while processing, the most likely reason is an out-of-memory error on the server. Refresh the page, reduce the number of GCMs or years, and try again. Or you can run the app locally from RStudio (recommended).</li>
<li>Questions or suggestions? Please email for <a href='mailto:caladaptr@gmail.com?subject=Chill Portions Shiny App'>feedback and support</a>.</li>
</ul>
</details>")
)
),
fluidRow(
column(12,
tags$br(),
tags$p("1. Select location(s)", class = "step topborder"))
),
fluidRow(
column(8,
tabsetPanel(
tabPanel("From Map",
tags$p("Select a location on the map, give it a name, and click 'Add to location list'. Repeat as needed.") %>%
shinytag_add_class("space_above_below"),
div(leafletOutput("mymap"),
style = "max-width:600px; margin-bottom:1em;"),
textInput("txtin_ptname_map", "Location name (optional): ", width = "680px", placeholder = "My Farm") %>%
shinytag_wrapchild2div("display:inline-block;") %>%
shinytag_add_class("space_above_below"),
actionButton("cmd_addloc_map", "Add to location list") %>%
shinytag_add_class("space_above_below") %>%
shinytag_add_class("indented2"),
htmlOutput("htmlout_addmap2lst_msg") %>%
shinytag_add_class("error-msg")
),
tabPanel("Enter Coordinates",
tags$p("Enter the longitude and latitude coordinates in decimal degrees separated by a comma, then click 'Add to locations'. Repeat if needed.") %>%
shinytag_add_class("space_above_below"),
textInput("txtin_coords", label = "Coordinates: ", placeholder = "-120.425, 37.365") %>%
shinytag_wrapchild2div("display:inline-block;") %>%
shinytag_add_class("space_above_below") %>%
shinytag_add_class("iblock"),
textInput("txtin_ptname_coords", "Location name (optional): ", width = "680px", placeholder = "My Farm") %>%
shinytag_wrapchild2div("display:inline-block;") %>%
shinytag_add_class("space_above_below"),
actionButton("cmd_addloc_coords", "Add to location list") %>%
shinytag_add_class("space_above_below"),
htmlOutput("htmlout_coords_not_dd_msg") %>%
shinytag_add_class("error-msg")
),
tabPanel("Upload CSV",
tags$p("Upload a CSV file below. Or you can load some ",
actionLink("cmd_sample_data", label = "sample data.")) %>%
shinytag_add_class("space_above_below"),
fileInput("infile_csv", "CSV File", multiple = FALSE) %>%
shinytag_add_class("iblock") %>%
helper(type = "markdown",
icon = "info-circle",
content = "csv_upload",
buttonLabel = "OK",
size = "m")
)
)),
column(4)
),
fluidRow(
column(12,
tags$p("2. Location List", class = "step topborder")
)
),
fluidRow(
column(4,
DT::dataTableOutput("outtbl_locations"),
tags$p(actionLink("cmd_clear_locs", "Clear all"),
style="text-align:right;"),
tags$p()
),
column(8)
),
fluidRow(
column(12,
tags$br(),
tags$p("3. Select the climate data to use as the ", tags$u("historic baseline"), class = "step topborder"),
sliderInput("base_year", "Year range:", min = 1950, max = 2005, value = c(1975, 2005), sep = "", step = 1) %>%
shinytag_add_class("iblock") %>%
helper(type = "markdown",
icon = "info-circle",
content = "years_select",
buttonLabel = "OK",
size = "m"),
htmlOutput("htmlout_basetime_msg") %>% shinytag_add_class("error-msg"),
selectInput("base_gcm", label = "GCMs:", choices = gcms[1:10], selected = gcms[1:4], multiple = TRUE) %>%
shinytag_add_class("iblock") %>%
helper(type = "markdown",
icon = "info-circle",
content = "base_gcm",
buttonLabel = "OK",
size = "l"),
selectInput("base_scenario", label = "Emissions scenario:", choices = scenarios[3], selected = scenarios[3], multiple = FALSE)
)
),
fluidRow(
column(12,
tags$br(),
tags$p("4. Select the climate data to use for the ", tags$u("projected future"), class = "step topborder"),
sliderInput("prj_year", "Year range:", min = 2006, max = 2099, value = c(2035, 2065), sep = "", step = 1) %>%
shinytag_add_class("iblock") %>%
helper(type = "markdown",
icon = "info-circle",
content = "year_select",
buttonLabel = "OK",
size = "m"),
htmlOutput("htmlout_prjtime_msg") %>% shinytag_add_class("error-msg"),
selectInput("prj_gcm", label = "GCMs:", choices = gcms[1:10], selected = gcms[1:4], multiple = TRUE) %>%
shinytag_add_class("iblock") %>%
helper(type = "markdown",
icon = "info-circle",
content = "prj_gcm",
buttonLabel = "OK",
size = "m"),
selectInput("prj_scenario", label = "Emissions scenarios:", choices = scenarios[1:2], selected = scenarios[1:2], multiple = TRUE) %>%
shinytag_add_class("iblock") %>%
helper(type = "markdown",
icon = "info-circle",
content = "prj_scenario",
buttonLabel = "OK",
size = "m")
)
),
fluidRow(
column(12,
tags$br(),
tags$p("5. Chill analysis options", class = "step topborder"),
tags$p(tags$strong("Start counting chill portions on the first day of:")),
div(
div(selectInput("chill_month", label = NULL, choices = month.abb, selected = "Sep",
multiple = FALSE, width = "90px"),
style = "display:inline-block; width:100px; vertical-align:top;")
),
tags$head(tags$style(type="text/css", "#safe_chill, #req_chill {width: 60px}")),
textInput("safe_chill", label = "Safe chill certainty level (%):", value = 90) %>%
shinytag_add_class("iblock") %>%
helper(type = "markdown",
icon = "info-circle",
content = "safe_chill",
buttonLabel = "OK",
size = "m"),
textInput("req_chill", label = "Required chill portions:", value = 48) %>%
shinytag_add_class("iblock") %>%
helper(type = "markdown",
icon = "info-circle",
content = "req_chill",
buttonLabel = "OK",
size = "m")
)
),
fluidRow(column(12,
tags$p("6. Compute chill", class = "step topborder"),
tags$div(
checkboxGroupInput("in_chkgrp_rptopts", "Report Options",
choices = c("Location Map" = "loc_map",
"Chill Distribution Histograms" = "chill_dist_hist",
"Safe Chill Table" = "safe_chill_tbl",
"Chill Portions by Month" = "chill_month_plot",
"Likelihood of Getting Required Chill" = "req_chill_tbl",
"Site comparison table" = "comp_table"),
selected = c("loc_map", "chill_dist_hist", "safe_chill_tbl",
"chill_month_plot", "req_chill_tbl", "comp_table")),
style = "margin-left:2em;"
),
actionButton("cmd_fetch2", "Fetch Data and Generate Report") %>%
shinytag_add_class("space_above_below"),
textOutput("txtout_loc_lst_empty") %>% shinytag_add_class("error-msg"),
textOutput("txtout_rpt_coming") %>% shinytag_add_class("happy-msg"),
htmlOutput("htmlout_rpt_link"),
textOutput("txt_status"),
tags$p())
),
includeHTML("igis_footer.html")
)
}
server <- function(input, output, session) {
connection_ok <- reactive({
## Do a quick check upon initialization if we have a connection with the Cal-Adapt
endpts_resp <- try(httr::GET(url = ca_baseurl))
good_connection <- class(endpts_resp)[1] != "try-error"
cat("good_connection = ", good_connection, "\n", sep = "")
good_connection
})
output$txtout_connect_status <- renderText({
if (connection_ok()) {
if (log_yn) {
log_entry_df <- data.frame(dt_utc = Sys.time(),
host = session$clientData$url_hostname,
path = session$clientData$url_pathname,
port = session$clientData$url_port,
shiny_port = shiny_port,
event = "Connected to Cal-Adapt API Upon Launch!!")
googlesheets4::sheet_append(ss = log_ss_id, data = log_entry_df, sheet = log_sheet)
}
NULL
} else {
if (log_yn) {
log_entry_df <- data.frame(dt_utc = Sys.time(),
host = session$clientData$url_hostname,
path = session$clientData$url_pathname,
port = session$clientData$url_port,
shiny_port = shiny_port,
event = "Could NOT connect to Cal-Adapt API at all")
googlesheets4::sheet_append(ss = log_ss_id, data = log_entry_df, sheet = log_sheet)
}
"The Cal-Adapt API is not currently reachable. Please refresh the page in a few minutes."
}
})
###############################################
## Set some bookmarking options
## Create a reactive val object to store the bookmark URL
bookmark_url <- reactiveVal()
## Assign a callback function to update the bookmark_url reactiveVal whenever bookmarking occurs
onBookmarked(function(url) {
bookmark_url(url)
})
## We don't want to record the state of command buttons (which if they were clicked would automatically
## run the observeEvent code on restore)
setBookmarkExclude(c("cmd_fetch2", "cmd_addloc_map", "cmd_addloc_coords"))
# Manually add a pipe-delimited copy of the locs_tbl() data frame to the bookmark state, so it can be
# transmitted via the bookmark URL
onBookmark(function(state) {
state$values$locations_csv <- paste(apply(locs_tbl(), 1, paste, collapse="|"),
collapse = "|")
})
# Read values from state$values when we restore
onRestore(function(state) {
rebuilt_df <- strsplit(state$values$locations_csv, "\\|")[[1]] %>%
matrix(ncol = 3, byrow = TRUE) %>%
as.data.frame() %>%
dplyr::rename(site = V1, lon = V2, lat = V3) %>%
dplyr::mutate(lon = as.numeric(lon), lat = as.numeric(lat))
locs_tbl(rebuilt_df)
})
## Add an observer to watch out for clicks on the shinyhelper buttons.
## The md files will be located in the default 'helpfiles' subdir
observe_helpers()
## Create a reactiveVal object for all the locations (to be filled with a data frame)
locs_tbl <- reactiveVal()
# ## Create a (hidden) reactive output that is the condition expression for a conditional panel
# rpt_coming_show <- reactiveVal(FALSE)
# output$rpt_coming_vis <- reactive({
# rpt_coming_show()
# })
# outputOptions(output, "rpt_coming_vis", suspendWhenHidden=FALSE)
## THIS DOESN'T WORK, I THINK BECAUSE IT DOESN'T HAVE AN OBSERVER
## outputOptions(output, "txtout_rpt_coming", priority = 10)
## Set the initial map extent (called once but never called again after that)
output$mymap <- renderLeaflet({
leaflet() %>%
addTiles() %>%
setView(-120.2, 36.4, zoom=6)
})
## The following will run whenever input$mymap_click changes
observeEvent(input$mymap_click, {
## Get the coordinates
click_coords_lst <- input$mymap_click
## Clear existing markers and add a marker at the new location
leafletProxy('mymap') %>%
clearMarkers() %>%
addMarkers(lng = click_coords_lst$lng,
lat = click_coords_lst$lat)
})
## The following will run whenever cmd_addloc_map is clicked
observeEvent(input$cmd_addloc_map, {
## Get the coordinates
click_coords_lst <- input$mymap_click
if (is.null(click_coords_lst)) {
output$htmlout_addmap2lst_msg <- renderUI(HTML("Error: Click a point on the map first"))
} else {
output$htmlout_addmap2lst_msg <- renderUI(NULL)
## Generate a location name if there isn't one
if (input$txtin_ptname_map == "") {
this_site <- paste0("Pt ", ifelse(is.null(locs_tbl()), 0, nrow(locs_tbl())) + 1)
} else {
this_site <- input$txtin_ptname_map
}
locs_tbl(locs_tbl() %>%
bind_rows(tibble(site = this_site,
lon = round(click_coords_lst$lng, 5),
lat = round(click_coords_lst$lat, 5)))
)
}
})
## The following will run whenever cmd_addloc_coords is clicked
observeEvent(input$cmd_addloc_coords, {
## Parse out the coordinates
mycoords <- stringr::str_split(input$txtin_coords, ",")[[1]] %>%
trimws() %>%
as.numeric()
if (NA %in% mycoords) {
output$htmlout_coords_not_dd_msg <- renderUI(HTML("Error: Please enter coordinates in decmial degrees separated by a comma.<br/>Example: -120.226, 36.450"))
} else {
## Clear error messages
output$htmlout_coords_not_dd_msg <- renderUI(NULL)
## TODO: Check if these coordinates are new
#if (!isTRUE(all.equal(pt_coords(), c(mycoords[1], mycoords[2]), tolerance = 0.0001))) {
if (TRUE) {
## Generate a location name if there isn't one
if (input$txtin_ptname_coords == "") {
this_site <- paste0("Pt ", ifelse(is.null(locs_tbl()), 0, nrow(locs_tbl())) + 1)
} else {
this_site <- input$txtin_ptname_coords
}
locs_tbl(locs_tbl() %>%
bind_rows(tibble(site = this_site,
lon = mycoords[1],
lat = mycoords[2])))
}
}
})
## The following will run whenever infile_csv changes
observeEvent(input$infile_csv, {
# input$infile_csv will be NULL initially. After the user selects
# and uploads a file, it will be a data frame with 'name',
# 'size', 'type', and 'datapath' columns. The 'datapath'
# column will contain the local filenames where the data can
# be found.
## Update the locs_tbl() reactiveVal object
locs_tbl(read.csv(input$infile_csv$datapath, header = TRUE))
})
## The following will run whenever cmd_sample_data link is clicked
observeEvent(input$cmd_sample_data, {
locs_tbl(read.csv('farms.csv', header = TRUE))
})
## The following will run whenever cmd_sample_data link is clicked
observeEvent(input$cmd_clear_locs, {
locs_tbl(NULL)
})
## If location changes or cmd_clear_locs is clicked, delete any messages
## below the 'fetch' button
observeEvent({
locs_tbl()
input$cmd_clear_locs
1
}, {
output$txtout_loc_lst_empty <- renderText(NULL)
## txtout_rpt_coming and htmlout_rpt_link are managed with shinyjs,
## because the render functions don't flush in time
shinyjs::html(id = "txtout_rpt_coming", html = "")
shinyjs::html(id = "htmlout_rpt_link", html = "")
})
## Update the outtbl_locations whenever locs_tbl() is updated
output$outtbl_locations <- DT::renderDataTable({
req(locs_tbl())
DT::datatable(locs_tbl(),
rownames= FALSE,
extensions = 'Buttons',
options = list(
paging = FALSE,
searching = FALSE,
fixedColumns = TRUE,
autoWidth = TRUE,
ordering = TRUE,
info = FALSE,
dom = 'tB',
buttons = c('copy', 'csv')
),
class = "display")
})
## Render an error messages for time periods that are too short - historic period
output$htmlout_basetime_msg <- renderUI({
req(input$base_year)
if (diff(input$base_year) < 10) {
HTML("Caution: this time period may be too short to be representative of climate. See info button for details.")
} else {
NULL
}
})
## Render an error messages for time periods that are too short - projected future
output$htmlout_prjtime_msg <- renderUI({
req(input$prj_year)
if (diff(input$prj_year) < 10) {
HTML("Caution: this time period may be too short to be representative of climate. See info button for details.")
} else {
NULL
}
})
## Fetch data and start the analysis
observeEvent(input$cmd_fetch2, {
## Check if a location has been selected
if (is.null(locs_tbl())) {
output$txtout_loc_lst_empty <- renderText("Location list empty!")
## output$txtout_rpt_coming <- renderText(NULL)
## output$htmlout_rpt_link <- renderUI(NULL)
shinyjs::html(id = "txtout_rpt_coming", html = "")
shinyjs::html(id = "htmlout_rpt_link", html = "")
NULL
} else {
## Erase any message that might be in txtout_loc_lst_empty
output$txtout_loc_lst_empty <- renderText(NULL)
# output$txtout_rpt_coming <- renderText(
# "Please wait. When the report is generated, the link will appear below. To keep a copy, be sure to save the HTML file to your computer, or print it."
# )
## Use shinyjs instead of renderXX() in order for the output to be refreshed immediately
shinyjs::html(id = "txtout_rpt_coming",
html = "Please wait, the report is being generated. When finished, the link will appear below.<br/>To keep a copy, be sure to save the HTML file to your computer, or print it.")
shinyjs::html(id = "htmlout_rpt_link", html = "")
## Record the report options as a character vector
rpt_opts <- isolate(input$in_chkgrp_rptopts)
## Trigger a bookmark update.
## In turn this will trigger the onBookmarked function which saves the
## current bookmark URL into the bookmark_url() reactiveVal (which we pass to report.Rmd)
session$doBookmark()
## Prepare some lists to hold objects created in the loop
site_info_lst <- list()
progress_lst <- list()
if (log_yn) {
fetch_start_time <- Sys.time()
log_entry_df <- data.frame(dt_utc = fetch_start_time,
host = session$clientData$url_hostname,
path = session$clientData$url_pathname,
port = session$clientData$url_port,
shiny_port = shiny_port,
event = paste0("Going to fetch data for ", nrow(locs_tbl()), " locations(s)"))
googlesheets4::sheet_append(ss = log_ss_id, data = log_entry_df, sheet = log_sheet)
}
for (i in 1:nrow(locs_tbl())) {
site_name <- locs_tbl()[i, 1, drop = TRUE]
x_coord <- as.numeric(locs_tbl()[i, 2])
y_coord <- as.numeric(locs_tbl()[i, 3])
## Create an API request object for projected climate
cur_prj_cap2 <- ca_loc_pt(coords = c(x_coord, y_coord)) %>%
ca_gcm(input$prj_gcm) %>%
ca_scenario(input$prj_scenario) %>%
ca_period("day") %>%
ca_cvar(c("tasmax", "tasmin")) %>%
ca_dates(start = paste(input$prj_year[1] - 1, which(input$chill_month == month.abb), "01", sep = "-"),
end = paste(input$prj_year[2], which(input$chill_month == month.abb) - 1, "30", sep = "-"))
###########################################
## FETCH AND PROCESS PROJECTED DATA
###########################################
## Create a progress object that we can pass to ca_getvals_tbl()
## Make sure it closes when we exit even if there's an error
progress_lst[[i]] <- Progress$new()
on.exit(progress_lst[[i]]$close())
progress_lst[[i]]$set(value = 0, message = paste0(site_name, ". Fetching projected data: "),
detail = "Daily min and max temp")
## Get values
pt_prj_dtemp_tbl2 <- try(ca_getvals_tbl(cur_prj_cap2,
quiet = TRUE,
shiny_progress = progress_lst[[i]]))
if (class(pt_prj_dtemp_tbl2)[1] == "try-error") {
if (log_yn) {
fetch_start_time <- Sys.time()
log_entry_df <- data.frame(dt_utc = fetch_start_time,
host = session$clientData$url_hostname,
path = session$clientData$url_pathname,
port = session$clientData$url_port,
shiny_port = shiny_port,
event = "Oh dear. An error occurred while fetching data")
googlesheets4::sheet_append(ss = log_ss_id, data = log_entry_df, sheet = log_sheet)
}
shinyjs::html(id = "txtout_rpt_coming", html = "")
shinyjs::html(id = "htmlout_rpt_link", html = "")
output$txtout_loc_lst_empty <- renderText("Oh dear. A network error occurred while fetching data. Please try again in about 30 minutes.")
return(NULL)
}
## Add Year, Month and Day, temp_c, growing season; pivot tasmin
progress_lst[[i]]$set(message = paste0(site_name, ". Processing:"),
detail = "Formatting projected data columns")
pt_prj_ymd_gs_wide_tbl2 <- pt_prj_dtemp_tbl2 %>%
mutate(Year = as.integer(substr(dt, 1, 4)),
Month = as.integer(substr(dt, 6, 7)),
Day = as.integer(substr(dt, 9, 10)),
temp_c = as.numeric(units::set_units(val, degC))) %>%
mutate(gs = case_when(Month < which(input$chill_month == month.abb) ~ Year,
Month >= which(input$chill_month == month.abb) ~ as.integer(Year + 1))) %>%
filter(!is.na(gs)) %>%
select(cvar, temp_c, Year, Month, Day, gcm, scenario, gs) %>%
tidyr::pivot_wider(names_from = cvar, values_from = temp_c) %>%
rename(Tmax = tasmax, Tmin = tasmin)
## recover some memory
# rm(pt_prj_dtemp_tbl2)
## Interpolate Hourly Temperatures in 'long' format
## This uses 'ca_make_hourly_temps', a modified version of chillr::make_hourly_temps
progress_lst[[i]]$set(message = paste0(site_name, ". Processing:"),
detail = "Interpolating projected hourly temps")
pt_prj_hrtmp_long2 <- pt_prj_ymd_gs_wide_tbl2 %>%
ca_make_hourly_temps(latitude = y_coord) %>%
mutate(date_hour = lubridate::make_datetime(Year, Month, Day, Hour, tz = "America/Los_Angeles")) %>%
arrange(date_hour)
## recover some memory
# rm(pt_prj_ymd_gs_wide_tbl2)
## Compute hourly chill portions
progress_lst[[i]]$set(message = paste0(site_name, ". Processing:"),
detail = "Computing projected hourly chill portions")
## Save a tibble with chill portions
chill_portions_prj_tbl2 <- pt_prj_hrtmp_long2 %>%
group_by(gs, gcm, scenario) %>%
mutate(accum_chill_prtn = Dynamic_Model(Temp))
## Compute the end-of-season chill for PROJECTED climate for each
## growing season, emissions scenario, and GCM,
pt_prj_eos_chill_tbl2 <- chill_portions_prj_tbl2 %>%
group_by(scenario, gs, gcm) %>%
summarise(max_chill = max(accum_chill_prtn), .groups = 'drop') %>%
mutate(time_period = paste0(isolate(input$prj_year[1]), "-", isolate(input$prj_year[2])))
###########################################
## FETCH AND PROCESS HISTORICAL DATA
###########################################
cur_base_cap2 <- ca_loc_pt(coords = c(x_coord, y_coord)) %>%
ca_gcm(input$base_gcm) %>%
ca_scenario(input$base_scenario) %>%
ca_period("day") %>%
ca_cvar(c("tasmax", "tasmin")) %>%
ca_dates(start = paste(input$base_year[1] - 1, which(input$chill_month == month.abb), "01", sep = "-"),
end = paste(input$base_year[2], which(input$chill_month == month.abb) - 1, "30", sep = "-"))
progress_lst[[i]]$set(message = paste0(site_name, ". Fetching historic data: "),
detail = "Daily min and max temp", value = 0)
pt_base_dtemp_tbl2 <- ca_getvals_tbl(cur_base_cap2,
quiet = TRUE,
shiny_progress = progress_lst[[i]])
progress_lst[[i]]$set(message = paste0(site_name, ". Processing:"),
detail = "Formatting historic data columns")
## Add Year, Month, Day growing season column and temp_c
pt_base_ymd_gs_wide_tbl2 <- pt_base_dtemp_tbl2 %>%
mutate(Year = as.integer(substr(dt, 1, 4)),
Month = as.integer(substr(dt, 6, 7)),
Day = as.integer(substr(dt, 9, 10)),
temp_c = as.numeric(units::set_units(val, degC))) %>%
mutate(gs = case_when(Month < which(input$chill_month == month.abb) ~ Year,
Month >= which(input$chill_month == month.abb) ~ as.integer(Year + 1))) %>%
filter(!is.na(gs)) %>%
select(cvar, temp_c, Year, Month, Day, gcm, scenario, gs) %>%
tidyr::pivot_wider(names_from = cvar, values_from = temp_c) %>%
rename(Tmax = tasmax, Tmin = tasmin)
## Compute hourly temperatures
progress_lst[[i]]$set(message = paste0(site_name, ". Processing:"),
detail = "Interpolating historic hourly temps")
pt_base_hrtmp_long2 <- pt_base_ymd_gs_wide_tbl2 %>%
ca_make_hourly_temps(latitude = y_coord) %>%
mutate(date_hour = lubridate::make_datetime(Year, Month, Day, Hour, tz = "America/Los_Angeles")) %>%
arrange(date_hour)
## Compute hourly chill portions
progress_lst[[i]]$set(message = paste0(site_name, ". Processing:"),
detail = "Computing historic hourly chill portions")
## Return the tibble with hourly chill portions column
chill_portions_base_tbl2 <- pt_base_hrtmp_long2 %>%
group_by(gs, gcm, scenario) %>%
mutate(accum_chill_prtn = Dynamic_Model(Temp))
## Compute the end-of-season chill for BASELINE climate for each
## growing season, emissions scenario, and GCM,
pt_base_eos_chill_tbl2 <- chill_portions_base_tbl2 %>%
group_by(scenario, gs, gcm) %>%
summarise(max_chill = max(accum_chill_prtn), .groups = 'drop') %>%
mutate(time_period = paste0(isolate(input$base_year[1]), "-", isolate(input$base_year[2])))
## Compute safe winter chill for each RCP
comb_safe_chill_tbl2 <- pt_base_eos_chill_tbl2 %>%
bind_rows(pt_prj_eos_chill_tbl2) %>%
group_by(time_period, scenario) %>%
summarize(safe_chill = quantile(max_chill, probs = 1 - as.numeric(input$safe_chill) / 100), .groups = "drop")
################################################################
## Compute the Histograms of EOS Chill
################################################################
if ("chill_dist_hist" %in% rpt_opts) {
## Compute the range of EOS chill for a uniform set of x-axis values for histograms
comb_eos_chill_range2 <- range(
range(pt_prj_eos_chill_tbl2 %>% pull(max_chill)),
range(pt_base_eos_chill_tbl2 %>% pull(max_chill)))
## Create the histogram of the EOS chill - baseline
gg_base_eos_chill <- ggplot(data = pt_base_eos_chill_tbl2, aes(x=max_chill)) +
geom_histogram() +
xlim(comb_eos_chill_range2) +
labs(title = "Historical: End-of-Season Chill Portion",
subtitle = paste0("Modelled historic data. ", isolate(input$base_year[1]), " - ", isolate(input$base_year[2])),
caption = paste0(
"Data source: Cal-Adapt.org\n",
stringr::str_wrap(paste0("GCMs: ", paste(isolate(input$base_gcm), collapse = ", ")),
width = 150)),
x = "end-of-season chill portion",
y = "count") +
geom_vline(data = comb_safe_chill_tbl2 %>%
filter(scenario == "historical"),
color = "red", aes(xintercept = safe_chill), size = 1) +
theme(plot.caption = element_text(hjust = 0),
plot.margin = margin(20, 20, 20, 20),
plot.background = element_rect(colour = "gray50", size = 3))
## Create the RCP45 histogram
eos_chill_45_tbl <- pt_prj_eos_chill_tbl2 %>%
filter(scenario == "rcp45")
if (nrow(eos_chill_45_tbl) == 0) {
gg_rcp45_eos_chill <- NA
} else {
gg_rcp45_eos_chill <- ggplot(data = eos_chill_45_tbl, aes(x=max_chill)) +
geom_histogram() +
xlim(comb_eos_chill_range2) +
labs(title = "RCP 4.5: End-of-Season Chill Portion",
subtitle = paste0(isolate(input$prj_year[1]), " - ", isolate(input$prj_year[2])),
caption = paste0(
"Data source: Cal-Adapt.org\n",
stringr::str_wrap(paste0("GCMs: ", paste(isolate(input$prj_gcm), collapse = ", ")),
width = 120)),
x = "end-of-season chill portion",
y = "count") +
geom_vline(data = comb_safe_chill_tbl2 %>%
filter(scenario == "rcp45"),
color = "red", aes(xintercept = safe_chill), size = 1) +
theme(plot.caption = element_text(hjust = 0),
plot.margin = margin(20, 20, 20, 20),
plot.background = element_rect(colour = "gray50", size = 3))
}
## Create the RCP85 histogram
eos_chill_85_tbl <- pt_prj_eos_chill_tbl2 %>%
filter(scenario == "rcp85")
if (nrow(eos_chill_85_tbl) == 0) {
gg_rcp85_eos_chill <- NA
} else {
gg_rcp85_eos_chill <- ggplot(data = eos_chill_85_tbl, aes(x=max_chill)) +
geom_histogram() +
xlim(comb_eos_chill_range2) +
labs(title = "RCP 8.5: End-of-Season Chill Portion",
subtitle = paste0(isolate(input$prj_year[1]), " - ", isolate(input$prj_year[2])),
caption = paste0(
"Data source: Cal-Adapt.org\n",
stringr::str_wrap(paste0("GCMs: ", paste(isolate(input$prj_gcm), collapse = ", ")),
width = 150)),
x = "end-of-season chill portion",
y = "count") +
geom_vline(data = comb_safe_chill_tbl2 %>%
filter(scenario == "rcp85"),
color = "red", aes(xintercept = safe_chill), size = 1) +
theme(plot.caption = element_text(hjust = 0),
plot.margin = margin(20, 20, 20, 20),
plot.background = element_rect(colour = "gray50", size = 3))
}
} else {
gg_base_eos_chill <- NA
gg_rcp45_eos_chill <- NA
gg_rcp85_eos_chill <- NA
}
################################################################
## Compute the Percentage of Runs that Meet the Required Chill
################################################################
if ("req_chill_tbl" %in% rpt_opts || "comp_table" %in% rpt_opts) {
## For each baseline scenario, compute the percent of runs that hit the required chill
base_req_chill_pct_tbl2 <- pt_base_eos_chill_tbl2 %>%
group_by(gs, gcm, scenario) %>%
summarise(reached_thresh = max(max_chill) >= as.numeric(input$req_chill), .groups = 'drop') %>%
group_by(scenario) %>%
summarise(percent_reached_thresh = sum(reached_thresh) / n(), .groups = 'drop') %>%
mutate(time_period = paste0(isolate(input$base_year[1]), "-", isolate(input$base_year[2])),
percent_reached_thresh = percent_reached_thresh) %>%
relocate(time_period, scenario, percent_reached_thresh)
## For each projected scenario, compute the percent of runs that hit the required chill
prj_req_chill_pct_tbl2 <- pt_prj_eos_chill_tbl2 %>%
group_by(gs, gcm, scenario) %>%
summarise(reached_thresh = max(max_chill) >= as.numeric(input$req_chill), .groups = 'drop') %>%
group_by(scenario) %>%
summarise(percent_reached_thresh = sum(reached_thresh) / n(), .groups = 'drop') %>%
mutate(time_period = paste0(isolate(input$prj_year[1]), "-", isolate(input$prj_year[2])),
percent_reached_thresh = percent_reached_thresh) %>%
relocate(time_period, scenario, percent_reached_thresh)
## Combine the baseline and projected tables of proportion of runs that reach required chill
comb_req_chill_pct_tbl2 <- base_req_chill_pct_tbl2 %>%
bind_rows(prj_req_chill_pct_tbl2)
} else {
comb_req_chill_pct_tbl2 <- NA
}
################################################################
## Generate the Time Series of Chill Portions Line Plots
################################################################
if ("chill_month_plot" %in% rpt_opts) {
## Render the chill portion time series for the historical period
## Convert the month to an integer
chill_month_int <- which(month.abb == isolate(input$chill_month))
## Generate the time series plot for the baseline
gg_chillts_base <- ggplot(data = chill_portions_base_tbl2 %>%
filter(Hour == 0) %>%
mutate(year_plot = if_else(Month >= chill_month_int, 1970, 1971)) %>%
mutate(date_plot = lubridate::make_date(year_plot, Month, Day),
gs_gcm = paste(gs, gcm, sep = "_")) %>%
select(scenario, gs, gcm, gs_gcm, date_plot, accum_chill_prtn),
aes(x = date_plot, y = accum_chill_prtn)) +
geom_line(aes(color=gs_gcm), show.legend = FALSE) +
geom_hline(color = "black", aes(yintercept = as.numeric(input$req_chill)), size = 1) +
scale_x_date(date_breaks = "1 month", date_labels = "%b") +
labs(title = "Historical: Chill Portion Accumulation",
subtitle = paste0(isolate(input$base_year[1]), " - ", isolate(input$base_year[2])),
caption = paste0(
"Data source: Cal-Adapt.org\n",
stringr::str_wrap(paste0("GCMs: ", paste(isolate(input$base_gcm), collapse = ", ")),
width = 80)),
x = NULL, y = "Chill Portion") +
theme(plot.caption = element_text(hjust = 0),
plot.margin = margin(20, 20, 20, 20),
plot.background = element_rect(colour = "gray50", size = 3))
## Generate the time series plot for the rcp45
gg_chillts_rcp45 <- ggplot(data = chill_portions_prj_tbl2 %>%
filter(Hour == 0, scenario == "rcp45") %>%
mutate(year_plot = if_else(Month >= chill_month_int, 1970, 1971)) %>%
mutate(date_plot = lubridate::make_date(year_plot, Month, Day),
gs_gcm = paste(gs, gcm, sep = "_")) %>%
select(scenario, gs, gcm, gs_gcm, date_plot, accum_chill_prtn),
aes(x = date_plot, y = accum_chill_prtn)) +
geom_line(aes(color=gs_gcm), show.legend = FALSE) +
geom_hline(color = "black", aes(yintercept = as.numeric(input$req_chill)), size = 1) +
scale_x_date(date_breaks = "1 month", date_labels = "%b") +
labs(title = "RCP 4.5: Chill Portion Accumulation",
subtitle = paste0(isolate(input$prj_year[1]), " - ", isolate(input$prj_year[2])),
caption = paste0(
"Data source: Cal-Adapt.org\n",
stringr::str_wrap(paste0("GCMs: ", paste(isolate(input$prj_gcm), collapse = ", ")),
width = 80)),
x = NULL, y = "Chill Portion") +
theme(plot.caption = element_text(hjust = 0),
plot.margin = margin(20, 20, 20, 20),
plot.background = element_rect(colour = "gray50", size = 3))
## Generate the time series plot for the rcp85
gg_chillts_rcp85 <- ggplot(data = chill_portions_prj_tbl2 %>%
filter(Hour == 0, scenario == "rcp85") %>%
mutate(year_plot = if_else(Month >= chill_month_int, 1970, 1971)) %>%
mutate(date_plot = lubridate::make_date(year_plot, Month, Day),
gs_gcm = paste(gs, gcm, sep = "_")) %>%
select(scenario, gs, gcm, gs_gcm, date_plot, accum_chill_prtn),
aes(x = date_plot, y = accum_chill_prtn)) +
geom_line(aes(color=gs_gcm), show.legend = FALSE) +
geom_hline(color = "black", aes(yintercept = as.numeric(input$req_chill)), size = 1) +
scale_x_date(date_breaks = "1 month", date_labels = "%b") +
labs(title = "RCP 8.5: Chill Portion Accumulation",
subtitle = paste0(isolate(input$prj_year[1]), " - ", isolate(input$prj_year[2])),
caption = paste0(
"Data source: Cal-Adapt.org\n",
stringr::str_wrap(paste0("GCMs: ", paste(isolate(input$prj_gcm), collapse = ", ")),
width = 80)),
x = NULL, y = "Chill Portion") +
theme(plot.caption = element_text(hjust = 0),
plot.margin = margin(20, 20, 20, 20),
plot.background = element_rect(colour = "gray50", size = 3))
} else {
gg_chillts_base <- NA
gg_chillts_rcp45 <- NA
gg_chillts_rcp85 <- NA
}
#####################################################
## DONE CREATING THE OUTPUTS FOR THIS LOCATION
#####################################################
progress_lst[[i]]$set(message = paste0(site_name, "."), detail = "Done")
## progress_lst[[i]]$close()
site_info_lst[[i]] <- list(site_name = site_name,
site_coords = c(x_coord, y_coord),
safe_chill_tbl = comb_safe_chill_tbl2,
gg_base_eos_chill = gg_base_eos_chill,
gg_rcp45_eos_chill = gg_rcp45_eos_chill,
gg_rcp85_eos_chill = gg_rcp85_eos_chill,
gg_chillts = list(gg_chillts_base = gg_chillts_base,
gg_chillts_rcp45 = gg_chillts_rcp45,
gg_chillts_rcp85 = gg_chillts_rcp85),
comb_req_chill_pct_tbl = comb_req_chill_pct_tbl2)
} ## for i in 1:nrow(locs_tbl())
## DONE FETCHING DATA
if (log_yn) {
cat("LETS LOG HOW LONG IT TOOK TO FETCH DATA! \n")
fetch_diff <- difftime(Sys.time(), fetch_start_time, units = "sec")
log_entry_df <- data.frame(dt_utc = Sys.time(),
host = session$clientData$url_hostname,
path = session$clientData$url_pathname,
port = session$clientData$url_port,
shiny_port = shiny_port,
event = paste0("Done! Total time to fetch data and generate summaries: ", round(as.numeric(fetch_diff),1), " secs"))
googlesheets4::sheet_append(ss = log_ss_id, data = log_entry_df, sheet = log_sheet)
}
progress_rpt <- Progress$new()
progress_rpt$set(message = "Generating report.", detail = "Please wait, this can take a minute")
##########################################
## Generate the report
##########################################
output_fn <- "report.html"
index_html <- rmarkdown::render("report.Rmd",
output_file = output_fn,
output_dir = "www",
params = list(bookmark_url = bookmark_url(),
rpt_opts = rpt_opts,
site_info_lst = site_info_lst,
model_settings_lst = list(
base_year = isolate(input$base_year),
base_gcm = isolate(input$base_gcm),
base_scenario = isolate(input$base_scenario),
prj_year = isolate(input$prj_year),
prj_gcm = isolate(input$prj_gcm),
prj_scenario = isolate(input$prj_scenario),
chill_month = isolate(input$chill_month),
safe_chill = isolate(input$safe_chill),
req_chill = isolate(input$req_chill))
)
)
## Close progress objects
suppressWarnings({
progress_rpt$close()
for (i in 1:length(progress_lst)) {
progress_lst[[i]]$close()
}
})
## Remove the content from txtout_rpt_coming
## output$txtout_rpt_coming <- renderText(NULL) ## doesn't always work
## shinyjs::html(id = "txtout_rpt_coming", html = "")
shinyjs::html(id = "htmlout_rpt_link",
html = "<p><a href=\"report.html\" target=\"_blank\" class=\"btn btn-default\" style=\"background-color:#e5f5ca; color:green; font-weight:bold; margin-left:2em;\" rel=\"noopener\">Open Report</a></p>")
## Open the report
## browseURL(output_fn) ## doesn't work on shinyapps.io
## js$browseMe(output_fn) ## creates a pop-up windows :-(
} ## if not is.null locs_tbl()
})
}
shinyApp(ui, server, enableBookmarking = "url")
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.