diff --git a/R/app_server.R b/R/app_server.R new file mode 100644 index 0000000..e0d6a77 --- /dev/null +++ b/R/app_server.R @@ -0,0 +1,371 @@ +# Server logic for St. Andrews Shiny App + +create_server <- function(input, output, session) { + # Load data + owners <- readRDS(app_config$data_paths$owners) + listings <- readRDS(app_config$data_paths$listings) |> + arrange(price_per_sqft) |> + mutate( + price_fmt = scales::dollar(price), + ppsf_fmt = scales::dollar(price_per_sqft) + ) + sales <- readRDS(app_config$data_paths$sales) |> + arrange(desc(listed_date)) |> + mutate( + price_fmt = scales::dollar(price), + ppsf_fmt = scales::dollar(price_per_sqft) + ) + last_sale_date <- format(attr(owners, "last_sale_date"), "%Y-%m-%d") + sbdvn <- sf::st_read(app_config$data_paths$plats) + beaches <- readRDS(app_config$data_paths$beaches) + + # update tabs depending on side panel + observeEvent(input$menu, { + updateF7Tabs(id = "tabs", + selected = input$menu) + }) + + # resources sub-section toggle + res_section <- reactiveVal("beaches") + observeEvent(input$res_beaches, { res_section("beaches") }) + observeEvent(input$res_links, { res_section("links") }) + observeEvent(input$res_restart, { res_section("restart") }) + + output$resources_content <- renderUI({ + if (res_section() == "beaches") { + tagList( + f7Card( + title = "Beaches", + divider = TRUE, + leafletOutput("beach_map"), + footer = p("Source: Environmental Protection Agency") + ), + f7Block( + h3("Helpful Links:"), + f7List( + inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE, + f7ListItem(title = "EPA Beaches", href = "https://www.epa.gov/beaches", external = TRUE), + f7ListItem(title = "Red Tide Forecast", href = "https://habforecast.gcoos.org/", external = TRUE), + f7ListItem(title = "Healthy Beaches Program", href = "https://fdoh.maps.arcgis.com/apps/instant/nearby/index.html?appid=7106a20597de4bff98cc5ebc7f932047&findSource=0&find=1600%2520Harbor%2520Dr%2520S%252C%2520Venice%252C%2520Florida%252C%252034285&sliderDistance=2", external = TRUE), + f7ListItem(title = "MOTE Beach Conditions", href = "https://visitbeaches.org/beach/6/report/53033", external = TRUE), + f7ListItem(title = "National Hurricane Center", href = "https://www.nhc.noaa.gov/", external = TRUE) + ) + ) + ) + } else { + tagList( + f7BlockTitle(title = "Services:", size = "medium"), + f7Block( + f7List( + mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE, + f7Link(label = "City of Venice", href = "https://www.venicegov.com/"), + f7Link(label = "Florida Power & Light", href = "https://www.fpl.com/"), + f7Link(label = "Sarasota County", href = "https://www.scgov.net/"), + f7Link(label = "Property Appraiser", href = "https://www.sc-pa.com/"), + f7Link(label = "Open GIS Portal", href = "https://data-sarco.opendata.arcgis.com/"), + f7Link(label = "Waste & Recycling", href = "https://www.venicegov.com/government/public-works/waste-and-recycling"), + f7Link(label = "Condo Regulation", href = "https://condos.myfloridalicense.com/"), + f7Link(label = "Property Records Search", href = "https://www.sarasotaclerk.com/records/official-records/search-land-records") + ) + ), + f7BlockTitle(title = "Documents:", size = "medium"), + f7Block( + f7List( + mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE, + f7Link(label = "St. Andrews Covenants", href = "docs/2000_01_01_st_andrews_covenants.pdf"), + f7Link(label = "St. Andrews (unrecorded)", href = "docs/2004_06_23_sap_map.pdf"), + f7Link(label = "Patios 2", href = "docs/1997_08_12_patios_2_plat.pdf"), + f7Link(label = "Patios 3", href = "docs/1998_11_17_patios_3_plat.pdf"), + f7Link(label = "Villas 2", href = "docs/1998_09_14_villas_2_plat.pdf") + ) + ) + ) + } + }) + + # Initialize filteredOwners with all owners + filteredOwners <- reactiveVal(owners) + + # Function to filter owners based on inputs + filterOwners <- function() { + filtered <- owners + + # Filter by subdivision if specified + if (!is.null(input$sub_name) && input$sub_name != "All") { + # Get the subdivision IDs for the selected subdivision name + sub_ids <- sbdvn %>% + filter(sub_name == input$sub_name) %>% + pull(max_sub_id) + filtered <- filtered %>% + filter(subdivision %in% sub_ids) + } + + # Filter by name if specified + if (!is.null(input$name) && input$name != "") { + filtered <- filtered %>% + filter( + grepl(input$name, owner_1, ignore.case = TRUE) | + grepl(input$name, owner_2, ignore.case = TRUE) + ) + } + + # Filter by location if specified + if (!is.null(input$location) && input$location != "") { + filtered <- filtered %>% + filter(grepl(input$location, location, ignore.case = TRUE)) + } + + return(filtered) + } + + # Update filtered owners when filter button is clicked + observeEvent(input$filterButton, { + filteredOwners(filterOwners()) + }) + + # Also update when any of the filter inputs change + observe({ + # Track dependencies + input$name + input$location + input$sub_name + + # Update filtered owners, but only if the filter button has been clicked at least once + # To prevent immediate filtering on app start + if (!is.null(input$filterButton)) { + if (input$filterButton > 0) { + filteredOwners(filterOwners()) + } + } + }) + + mean_lat <- reactive({ + if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) { + filteredOwners() %>% + st_coordinates() %>% + .[, "Y"] %>% + mean() + } else { + app_config$map_config$default_lat + } + }) + + mean_lng <- reactive({ + if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) { + filteredOwners() %>% + st_coordinates() %>% + .[, "X"] %>% + mean() + } else { + app_config$map_config$default_lng + } + }) + + + output$map <- renderLeaflet({ + leaflet() %>% + addProviderTiles("CartoDB.Voyager") %>% + addPolygons( + data = sbdvn, + color = "red", + weight = 2, + opacity = 0.5, + fillOpacity = 0.2, + label = ~sub_name, + group = "Subdivisions" + ) + }) + + # Update map markers when filteredOwners changes + observe({ + leafletProxy("map") %>% + clearGroup("Owners") %>% + addMarkers( + data = filteredOwners(), + popup = popupTable( + filteredOwners(), + row.numbers = FALSE, + feature.id = FALSE, + zcol = c( + "label", + "owner_1", + "owner_2" + ) + ), + group = "Owners" + ) %>% + addLayersControl( + overlayGroups = c("Subdivisions", "Owners"), + options = layersControlOptions(collapsed = FALSE) + ) %>% + setView(lng = mean_lng(), lat = mean_lat(), zoom = app_config$map_config$default_zoom) + }) + + output$table <- renderDT({ + if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) { + my_table <- + filteredOwners() %>% + st_drop_geometry() %>% + select(label, owner_1, owner_2, homestead) + datatable(my_table, + colnames = c("Address", "Owner 1", "Owner 2", "Homestead"), + rownames = FALSE, + options = list( + pageLength = 10, + scrollX = TRUE, + searching = FALSE, + lengthMenu = c(5, 10, 25, 50), + dom = 'tpi' + ) + ) + } else { + # Return empty table with same structure + my_table <- data.frame( + label = character(0), + owner_1 = character(0), + owner_2 = character(0), + homestead = numeric(0) + ) + datatable(my_table, + colnames = c("Address", "Owner 1", "Owner 2", "Homestead"), + rownames = FALSE, + options = list( + pageLength = 10, + scrollX = TRUE, + searching = FALSE, + lengthMenu = c(5, 10, 25, 50), + dom = 'tpi' + ) + ) + } + }) + prep_mailing <- function(data) { + data |> + sf::st_drop_geometry() |> + dplyr::select(owner_1, owner_2, mailing_address_1, mailing_address_2, + mailing_city, mailing_state, mailing_zip_code) |> + dplyr::mutate( + owner_2 = ifelse(is.na(owner_2), "", owner_2), + mailing_address_2 = ifelse(is.na(mailing_address_2), "", mailing_address_2) + ) |> + dplyr::rename( + address_1 = mailing_address_1, + address_2 = mailing_address_2, + city = mailing_city, + state = mailing_state, + zip = mailing_zip_code + ) + } + + output$download_filtered <- downloadHandler( + filename = "st_andrews_owners_filtered.csv", + content = function(file) { + write.csv(prep_mailing(filteredOwners()), file, row.names = FALSE) + } + ) + + output$download_all <- downloadHandler( + filename = "st_andrews_owners_all.csv", + content = function(file) { + write.csv(prep_mailing(owners), file, row.names = FALSE) + } + ) + +# listings map ---- + output$listings_map <- renderLeaflet({ + leaflet(listings) |> + addProviderTiles("CartoDB.Voyager") |> + addPolygons( + data = sbdvn, + color = "red", + weight = 2, + opacity = 0.5, + fillOpacity = 0.2, + label = ~sub_name + ) |> + addMarkers( + lat = ~latitude, + lng = ~longitude, + popup = ~paste0( + "", address, "
", + "Price: ", price_fmt, "
", + "Sq Ft: ", sqft, "
", + "$/Sq Ft: ", ppsf_fmt + ) + ) |> + setView(lng = app_config$map_config$default_lng, + lat = app_config$map_config$default_lat, + zoom = app_config$map_config$default_zoom) + }) + +# listings table ---- + output$listings_table <- renderDT({ + datatable( + listings |> select(listed_date, address, sqft, price_fmt, ppsf_fmt), + colnames = c("Listed", "Address", "Sq Ft", "Price", "$/Sq Ft"), + rownames = FALSE, + options = list( + pageLength = 25, + searching = FALSE, + dom = 't' + ) + ) + }) + +# sales map ---- + output$sales_map <- renderLeaflet({ + leaflet(sales) |> + addProviderTiles("CartoDB.Voyager") |> + addPolygons( + data = sbdvn, + color = "red", + weight = 2, + opacity = 0.5, + fillOpacity = 0.2, + label = ~sub_name + ) |> + addMarkers( + lat = ~latitude, + lng = ~longitude, + popup = ~paste0( + "", address, "
", + "Sale Date: ", listed_date, "
", + "Price: ", price_fmt, "
", + "Sq Ft: ", sqft, "
", + "$/Sq Ft: ", ppsf_fmt + ) + ) |> + setView(lng = app_config$map_config$default_lng, + lat = app_config$map_config$default_lat, + zoom = app_config$map_config$default_zoom) + }) + +# sales table ---- + output$sales_table <- renderDT({ + datatable( + sales |> select(listed_date, address, sqft, price_fmt, ppsf_fmt), + colnames = c("Date", "Address", "Sq Ft", "Price", "$/Sq Ft"), + rownames = FALSE, + options = list( + pageLength = 10, + searching = FALSE, + dom = 't' + ) + ) + }) + +# beach map ---- + output$beach_map <- renderLeaflet({ + leaflet() %>% + addProviderTiles("CartoDB.Voyager") %>% + setView(lng = app_config$map_config$beach_lng, + lat = app_config$map_config$beach_lat, + zoom = app_config$map_config$beach_zoom) %>% + addMarkers( + data = beaches, + lat = ~lat, + lng = ~lng, + popup = ~beach_name + ) + }) +} diff --git a/R/app_ui.R b/R/app_ui.R new file mode 100644 index 0000000..f752919 --- /dev/null +++ b/R/app_ui.R @@ -0,0 +1,232 @@ +# UI components for St. Andrews Shiny App + +create_ui <- function() { + f7Page( + title = app_config$app_config$title, + ## header ---- + tags$head( + tags$link(rel = "manifest", href = "manifest.json"), + tags$link(rel = "apple-touch-icon", sizes = "180x180", href = "images/apple-touch-icon.png"), + tags$meta(name = "apple-mobile-web-app-capable", content = "yes"), + tags$meta(name = "apple-mobile-web-app-status-bar-style", content = "default"), + tags$style(HTML(" + .dataTables_wrapper { + color: white; + } + .dataTables_wrapper table.dataTable thead th, + .dataTables_wrapper table.dataTable tbody td { + color: white; + } + .dataTables_wrapper table.dataTable tbody tr.odd { + background-color: #333; + } + .dataTables_wrapper table.dataTable tbody tr.even { + background-color: #444; + } + .dataTables_wrapper .dataTables_paginate .paginate_button { + color: white !important; + } + .dataTables_wrapper .dataTables_paginate .paginate_button.current{ + color: black !important; + } + .dataTables_filter { + display: none; /* Hide the search box */ + } + ")) + ), + ## options ---- + options = list( + theme = app_config$app_config$theme, + dark = app_config$app_config$dark, + pullToRefresh = TRUE + ), + ## layout ---- + f7TabLayout( + ### panels ---- + panels = tagList( + f7Panel( + id = "panel-left", + side = "left", + effect = "push", + title = "Menu", + f7PanelMenu( + id = "menu", + f7PanelItem( + tabName = "About", + title = "About", + icon = f7Icon("info_circle"), + active = TRUE + ), + f7PanelItem( + tabName = "Owners", + title = "Owners", + icon = f7Icon("person_2_fill") + ), + f7PanelItem( + tabName = "Listings", + title = "Listings", + icon = f7Icon("tag_fill") + ), + f7PanelItem( + tabName = "Sales", + title = "Sales", + icon = f7Icon("dollarsign_circle_fill") + ), + f7PanelItem( + tabName = "Resources", + title = "Resources", + icon = f7Icon("hammer_fill") + ) + ) + ) + ), + ### navbar ---- + navbar = f7Navbar( + title = app_config$app_config$title, + hairline = TRUE, + leftPanel = TRUE + ), + ### begin tabs ---- + f7Tabs( + id = "tabs", + animated = TRUE, + #### about ---- + f7Tab( + title = "About", + tabName = "About", + icon = f7Icon("info_circle"), + active = TRUE, + f7Card( + title = "About", + divider = "TRUE", + tags$img(src = "images/st_andrews.jpg", width = "100%"), + "St. Andrews Park is located in Venice, Florida. The condominiums are a mix of single-family homes, villas (2 and 4 units) and multi-unit buildings (8 units). There are 388 separate properties within 14 subdivisions. St. Andrews Park is one of many communities in the Plantation Golf and Country Club. It is should not be confused with the adjacent community St. Andrews East.", + footer = tagList( + f7Link("More Info", href = "http://www.cpmi.us/standrews-plantation/outside_home.asp") + ) + ) + ), + #### owners ---- + f7Tab( + title = "Owners", + tabName = "Owners", + icon = f7Icon("person_2_fill"), + f7Card( + title = "Owners:", + divider = TRUE, + raised = TRUE, + footer = paste0("Last sale date: ", last_sale_date), + f7List( + inset = TRUE, + dividers = TRUE, + strong = TRUE, + outline = FALSE, + f7Text( + inputId = "name", + label = "Last Name:", + placeholder = "\"Patel\"" + ), + f7Text( + inputId = "location", + label = "Address:", + placeholder = "\"123 Chalmers\"" + ), + f7Select( + inputId = "sub_name", + label = "Select Subdivision:", + choices = c("All", sort(sbdvn$sub_name)) + ), + tags$br(), + f7Button( + inputId = "filterButton", + label = "Find Owners", + icon = "", + color = "blue" + ) + ) + ), + f7Card( + title = "Map:", + divider = TRUE, + leafletOutput("map") + ), + f7Card( + title = "Table:", + divider = TRUE, + DTOutput("table") + ), + f7Card( + title = "Download:", + divider = TRUE, + f7List( + inset = TRUE, + downloadButton("download_filtered", "Download Filtered"), + downloadButton("download_all", "Download All (388)") + ) + ) + ), + #### listings ---- + f7Tab( + title = "Listings", + tabName = "Listings", + icon = f7Icon("tag_fill"), + f7Card( + title = "Map:", + divider = TRUE, + leafletOutput("listings_map") + ), + f7Card( + title = "Active Listings:", + divider = TRUE, + DTOutput("listings_table") + ) + ), + #### sales ---- + f7Tab( + title = "Sales", + tabName = "Sales", + icon = f7Icon("dollarsign_circle_fill"), + f7Card( + title = "Map:", + divider = TRUE, + leafletOutput("sales_map") + ), + f7Card( + title = "Recent Sales:", + divider = TRUE, + DTOutput("sales_table") + ) + ), + #### resources ---- + f7Tab( + title = "Resources", + tabName = "Resources", + icon = f7Icon("hammer_fill"), + f7Segment( + f7Button(inputId = "res_beaches", label = "Beaches"), + f7Button(inputId = "res_links", label = "Links"), + f7Button(inputId = "res_restart", label = "Restart") + ), + uiOutput("resources_content") + ) + ### end tabs---- + ), + ### begin scripts ---- + tags$script( + HTML( + " + $(document).on('click', '#pdfLink', function(event) { + event.preventDefault(); + window.open($(this).attr('href'), '_blank'); + }); + $(document).on('click', '#download_filtered, #download_all', function(event) { + event.preventDefault(); + window.open($(this).attr('href'), '_self'); + }); + " + ) + ) + ### end scripts ---- + ) + ) +} diff --git a/R/config.R b/R/config.R new file mode 100644 index 0000000..fbf32f4 --- /dev/null +++ b/R/config.R @@ -0,0 +1,38 @@ +# Configuration file for St. Andrews Shiny App + +app_config <- list( + data_paths = list( + owners = "./data/owners.rds", + plats = "./data/plats/plats.shp", + beaches = "./data/beaches.rds", + listings = "./data/listings.rds", + sales = "./data/sales.rds" + ), + update_config = list( + subdivisions = c("8120", "8113", "8171", "8195", "8221", "8163", + "8240", "8159", "8149", "8110", "8254", "8215", "8143"), + timeout_seconds = 300 + ), + map_config = list( + default_lng = -82.362253, + default_lat = 27.076199, + default_zoom = 16, + beach_lng = -82.4603, + beach_lat = 27.0999, + beach_zoom = 12 + ), + app_config = list( + title = "St. Andrews Park", + theme = "ios", + dark = TRUE + ) +) + +# Helper function to get data path +get_data_path <- function(data_type) { + if (data_type %in% names(app_config$data_paths)) { + return(app_config$data_paths[[data_type]]) + } else { + stop(paste("Unknown data type:", data_type)) + } +} diff --git a/app.R b/app.R index 71dbe00..c0f1e4c 100644 --- a/app.R +++ b/app.R @@ -1,4 +1,7 @@ -# load libraries ---- +# St. Andrews Shiny App - Main entry point +# Modularized version with separate UI and server components + +# Load libraries library(shiny) library(shinyMobile) library(leaflet) @@ -7,603 +10,16 @@ library(dplyr) library(leafpop) library(DT) -# load data ---- -owners <- readRDS("./data/owners.rds") -listings <- readRDS("./data/listings.rds") |> - arrange(price_per_sqft) |> - mutate( - price_fmt = scales::dollar(price), - ppsf_fmt = scales::dollar(price_per_sqft) - ) -sales <- readRDS("./data/sales.rds") |> - arrange(desc(listed_date)) |> - mutate( - price_fmt = scales::dollar(price), - ppsf_fmt = scales::dollar(price_per_sqft) - ) -last_sale_date <- format(attr(owners, "last_sale_date"), "%Y-%m-%d") -sbdvn <- sf::st_read("./data/plats/plats.shp") -beaches <- readRDS("./data/beaches.rds") +# Load configuration +source("./R/config.R") -# define ui ---- -ui <- f7Page( - title = "St. Andrews", - ## header ---- - tags$head( - tags$link(rel = "manifest", href = "manifest.json"), - tags$link(rel = "apple-touch-icon", sizes = "180x180", href = "images/apple-touch-icon.png"), - tags$meta(name = "apple-mobile-web-app-capable", content = "yes"), - tags$meta(name = "apple-mobile-web-app-status-bar-style", content = "default"), - tags$style(HTML(" - .dataTables_wrapper { - color: white; - } - .dataTables_wrapper table.dataTable thead th, - .dataTables_wrapper table.dataTable tbody td { - color: white; - } - .dataTables_wrapper table.dataTable tbody tr.odd { - background-color: #333; - } - .dataTables_wrapper table.dataTable tbody tr.even { - background-color: #444; - } - .dataTables_wrapper .dataTables_paginate .paginate_button { - color: white !important; - } - .dataTables_wrapper .dataTables_paginate .paginate_button.current{ - color: black !important; - } - .dataTables_filter { - display: none; /* Hide the search box */ - } - ")) - ), - ## options ---- - options = list( - theme = "ios", - dark = TRUE, - pullToRefresh = TRUE - ), - ## layout ---- - f7TabLayout( - ### panels ---- - panels = tagList( - f7Panel( - id = "panel-left", - side = "left", - effect = "push", - title = "Menu", - f7PanelMenu( - id = "menu", - f7PanelItem( - tabName = "About", - title = "About", - icon = f7Icon("info_circle"), - active = TRUE - ), - f7PanelItem( - tabName = "Owners", - title = "Owners", - icon = f7Icon("person_2_fill") - ), - f7PanelItem( - tabName = "Listings", - title = "Listings", - icon = f7Icon("tag_fill") - ), - f7PanelItem( - tabName = "Sales", - title = "Sales", - icon = f7Icon("dollarsign_circle_fill") - ), - f7PanelItem( - tabName = "Resources", - title = "Resources", - icon = f7Icon("hammer_fill") - ) - ) - ) - ), - ### navbar ---- - navbar = f7Navbar( - title = "St. Andrews Park", - hairline = TRUE, - leftPanel = TRUE - ), - ### begin tabs ---- - f7Tabs( - id = "tabs", - animated = TRUE, - #### about ---- - f7Tab( - title = "About", - tabName = "About", - icon = f7Icon("info_circle"), - active = TRUE, - f7Card( - title = "About", - divider = "TRUE", - tags$img(src = "images/st_andrews.jpg", width = "100%"), - "St. Andrews Park is located in Venice, Florida. The condominiums are a mix of single-family homes, villas (2 and 4 units) and multi-unit buildings (8 units). There are 388 separate properties within 14 subdivisions. St. Andrews Park is one of many communities in the Plantation Golf and Country Club. It is should not be confused with the adjacent community St. Andrews East.", - footer = tagList( - f7Link("More Info", href = "http://www.cpmi.us/standrews-plantation/outside_home.asp") - ) - ) - ), - #### owners ---- - f7Tab( - title = "Owners", - tabName = "Owners", - icon = f7Icon("person_2_fill"), - f7Card( - title = "Owners:", - divider = TRUE, - raised = TRUE, - footer = paste0("Last sale date: ", last_sale_date), - f7List( - inset = TRUE, - dividers = TRUE, - strong = TRUE, - outline = FALSE, - f7Text( - inputId = "name", - label = "Last Name:", - placeholder = "\"Patel\"" - ), - f7Text( - inputId = "location", - label = "Address:", - placeholder = "\"123 Chalmers\"" - ), - f7Select( - inputId = "sub_name", - label = "Select Subdivision:", - choices = c("All", sort(sbdvn$sub_name)) - ), - tags$br(), - f7Button( - inputId = "filterButton", - label = "Find Owners", - icon = "", - color = "blue" - ) - ) - ), - f7Card( - title = "Map:", - divider = TRUE, - leafletOutput("map") - ), - f7Card( - title = "Table:", - divider = TRUE, - DTOutput("table") - ), - f7Card( - title = "Download:", - divider = TRUE, - f7List( - inset = TRUE, - downloadButton("download_filtered", "Download Filtered"), - downloadButton("download_all", "Download All (388)") - ) - ) - ), - #### listings ---- - f7Tab( - title = "Listings", - tabName = "Listings", - icon = f7Icon("tag_fill"), - f7Card( - title = "Map:", - divider = TRUE, - leafletOutput("listings_map") - ), - f7Card( - title = "Active Listings:", - divider = TRUE, - DTOutput("listings_table") - ) - ), - #### sales ---- - f7Tab( - title = "Sales", - tabName = "Sales", - icon = f7Icon("dollarsign_circle_fill"), - f7Card( - title = "Map:", - divider = TRUE, - leafletOutput("sales_map") - ), - f7Card( - title = "Recent Sales:", - divider = TRUE, - DTOutput("sales_table") - ) - ), - #### resources ---- - f7Tab( - title = "Resources", - tabName = "Resources", - icon = f7Icon("hammer_fill"), - f7Segment( - f7Button(inputId = "res_beaches", label = "Beaches"), - f7Button(inputId = "res_links", label = "Links"), - f7Button(inputId = "res_restart", label = "Restart") - ), - uiOutput("resources_content") - ) - ### end tabs---- - ), - ### begin scripts ---- - tags$script( - HTML( - " - $(document).on('click', '#pdfLink', function(event) { - event.preventDefault(); - window.open($(this).attr('href'), '_blank'); - }); - $(document).on('click', '#download_filtered, #download_all', function(event) { - event.preventDefault(); - window.open($(this).attr('href'), '_self'); - }); - " - ) - ) - ### end scripts ---- - ) -) -# end ui ---- +# Load UI and server components +source("./R/app_ui.R") +source("./R/app_server.R") - -# define server ---- -server <- function(input, output, session) { - # update tabs depending on side panel - observeEvent(input$menu, { - updateF7Tabs(id = "tabs", - selected = input$menu) - }) - - # resources sub-section toggle - res_section <- reactiveVal("beaches") - observeEvent(input$res_beaches, { res_section("beaches") }) - observeEvent(input$res_links, { res_section("links") }) - observeEvent(input$res_restart, { res_section("restart") }) - - output$resources_content <- renderUI({ - if (res_section() == "beaches") { - tagList( - f7Card( - title = "Beaches", - divider = TRUE, - leafletOutput("beach_map"), - footer = p("Source: Environmental Protection Agency") - ), - f7Block( - h3("Helpful Links:"), - f7List( - inset = TRUE, dividers = TRUE, strong = TRUE, outline = FALSE, - f7ListItem(title = "EPA Beaches", href = "https://www.epa.gov/beaches", external = TRUE), - f7ListItem(title = "Red Tide Forecast", href = "https://habforecast.gcoos.org/", external = TRUE), - f7ListItem(title = "Healthy Beaches Program", href = "https://fdoh.maps.arcgis.com/apps/instant/nearby/index.html?appid=7106a20597de4bff98cc5ebc7f932047&findSource=0&find=1600%2520Harbor%2520Dr%2520S%252C%2520Venice%252C%2520Florida%252C%252034285&sliderDistance=2", external = TRUE), - f7ListItem(title = "MOTE Beach Conditions", href = "https://visitbeaches.org/beach/6/report/53033", external = TRUE), - f7ListItem(title = "National Hurricane Center", href = "https://www.nhc.noaa.gov/", external = TRUE) - ) - ) - ) - } else { - tagList( - f7BlockTitle(title = "Services:", size = "medium"), - f7Block( - f7List( - mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE, - f7Link(label = "City of Venice", href = "https://www.venicegov.com/"), - f7Link(label = "Florida Power & Light", href = "https://www.fpl.com/"), - f7Link(label = "Sarasota County", href = "https://www.scgov.net/"), - f7Link(label = "Property Appraiser", href = "https://www.sc-pa.com/"), - f7Link(label = "Open GIS Portal", href = "https://data-sarco.opendata.arcgis.com/"), - f7Link(label = "Waste & Recycling", href = "https://www.venicegov.com/government/public-works/waste-and-recycling"), - f7Link(label = "Condo Regulation", href = "https://condos.myfloridalicense.com/"), - f7Link(label = "Property Records Search", href = "https://www.sarasotaclerk.com/records/official-records/search-land-records") - ) - ), - f7BlockTitle(title = "Documents:", size = "medium"), - f7Block( - f7List( - mode = "links", inset = TRUE, outline = TRUE, dividers = TRUE, strong = TRUE, - f7Link(label = "St. Andrews Covenants", href = "docs/2000_01_01_st_andrews_covenants.pdf"), - f7Link(label = "St. Andrews (unrecorded)", href = "docs/2004_06_23_sap_map.pdf"), - f7Link(label = "Patios 2", href = "docs/1997_08_12_patios_2_plat.pdf"), - f7Link(label = "Patios 3", href = "docs/1998_11_17_patios_3_plat.pdf"), - f7Link(label = "Villas 2", href = "docs/1998_09_14_villas_2_plat.pdf") - ) - ) - ) - } - }) - - # Initialize filteredOwners with all owners - filteredOwners <- reactiveVal(owners) - - # Function to filter owners based on inputs - filterOwners <- function() { - filtered <- owners - - # Filter by subdivision if specified - if (!is.null(input$sub_name) && input$sub_name != "All") { - # Get the subdivision IDs for the selected subdivision name - sub_ids <- sbdvn %>% - filter(sub_name == input$sub_name) %>% - pull(max_sub_id) - filtered <- filtered %>% - filter(subdivision %in% sub_ids) - } - - # Filter by name if specified - if (!is.null(input$name) && input$name != "") { - filtered <- filtered %>% - filter( - grepl(input$name, owner_1, ignore.case = TRUE) | - grepl(input$name, owner_2, ignore.case = TRUE) - ) - } - - # Filter by location if specified - if (!is.null(input$location) && input$location != "") { - filtered <- filtered %>% - filter(grepl(input$location, location, ignore.case = TRUE)) - } - - return(filtered) - } - - # Update filtered owners when filter button is clicked - observeEvent(input$filterButton, { - filteredOwners(filterOwners()) - }) - - # Also update when any of the filter inputs change - observe({ - # Track dependencies - input$name - input$location - input$sub_name - - # Update filtered owners, but only if the filter button has been clicked at least once - # To prevent immediate filtering on app start - if (!is.null(input$filterButton)) { - if (input$filterButton > 0) { - filteredOwners(filterOwners()) - } - } - }) - - mean_lat <- reactive({ - if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) { - filteredOwners() %>% - st_coordinates() %>% - .[, "Y"] %>% - mean() - } else { - 27.076199 - } - }) - - mean_lng <- reactive({ - if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) { - filteredOwners() %>% - st_coordinates() %>% - .[, "X"] %>% - mean() - } else { - -82.362253 - } - }) - - - output$map <- renderLeaflet({ - leaflet() %>% - addProviderTiles("CartoDB.Voyager") %>% - addPolygons( - data = sbdvn, - color = "red", - weight = 2, - opacity = 0.5, - fillOpacity = 0.2, - label = ~sub_name, - group = "Subdivisions" - ) - }) - - # Update map markers when filteredOwners changes - observe({ - leafletProxy("map") %>% - clearGroup("Owners") %>% - addMarkers( - data = filteredOwners(), - popup = popupTable( - filteredOwners(), - row.numbers = FALSE, - feature.id = FALSE, - zcol = c( - "label", - "owner_1", - "owner_2" - ) - ), - group = "Owners" - ) %>% - addLayersControl( - overlayGroups = c("Subdivisions", "Owners"), - options = layersControlOptions(collapsed = FALSE) - ) %>% - setView(lng = mean_lng(), lat = mean_lat(), zoom = 16) - }) - - output$table <- renderDT({ - if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) { - my_table <- - filteredOwners() %>% - st_drop_geometry() %>% - select(label, owner_1, owner_2, homestead) - datatable(my_table, - colnames = c("Address", "Owner 1", "Owner 2", "Homestead"), - rownames = FALSE, - options = list( - pageLength = 10, - scrollX = TRUE, - searching = FALSE, - lengthMenu = c(5, 10, 25, 50), - dom = 'tpi' - ) - ) - } else { - # Return empty table with same structure - my_table <- data.frame( - label = character(0), - owner_1 = character(0), - owner_2 = character(0), - homestead = numeric(0) - ) - datatable(my_table, - colnames = c("Address", "Owner 1", "Owner 2", "Homestead"), - rownames = FALSE, - options = list( - pageLength = 10, - scrollX = TRUE, - searching = FALSE, - lengthMenu = c(5, 10, 25, 50), - dom = 'tpi' - ) - ) - } - }) - prep_mailing <- function(data) { - data |> - sf::st_drop_geometry() |> - dplyr::select(owner_1, owner_2, mailing_address_1, mailing_address_2, - mailing_city, mailing_state, mailing_zip_code) |> - dplyr::mutate( - owner_2 = ifelse(is.na(owner_2), "", owner_2), - mailing_address_2 = ifelse(is.na(mailing_address_2), "", mailing_address_2) - ) |> - dplyr::rename( - address_1 = mailing_address_1, - address_2 = mailing_address_2, - city = mailing_city, - state = mailing_state, - zip = mailing_zip_code - ) - } - - output$download_filtered <- downloadHandler( - filename = "st_andrews_owners_filtered.csv", - content = function(file) { - write.csv(prep_mailing(filteredOwners()), file, row.names = FALSE) - } - ) - - output$download_all <- downloadHandler( - filename = "st_andrews_owners_all.csv", - content = function(file) { - write.csv(prep_mailing(owners), file, row.names = FALSE) - } - ) - -# listings map ---- - output$listings_map <- renderLeaflet({ - leaflet(listings) |> - addProviderTiles("CartoDB.Voyager") |> - addPolygons( - data = sbdvn, - color = "red", - weight = 2, - opacity = 0.5, - fillOpacity = 0.2, - label = ~sub_name - ) |> - addMarkers( - lat = ~latitude, - lng = ~longitude, - popup = ~paste0( - "", address, "
", - "Price: ", price_fmt, "
", - "Sq Ft: ", sqft, "
", - "$/Sq Ft: ", ppsf_fmt - ) - ) |> - setView(lng = -82.362253, lat = 27.076199, zoom = 16) - }) - -# listings table ---- - output$listings_table <- renderDT({ - datatable( - listings |> select(listed_date, address, sqft, price_fmt, ppsf_fmt), - colnames = c("Listed", "Address", "Sq Ft", "Price", "$/Sq Ft"), - rownames = FALSE, - options = list( - pageLength = 25, - searching = FALSE, - dom = 't' - ) - ) - }) - -# sales map ---- - output$sales_map <- renderLeaflet({ - leaflet(sales) |> - addProviderTiles("CartoDB.Voyager") |> - addPolygons( - data = sbdvn, - color = "red", - weight = 2, - opacity = 0.5, - fillOpacity = 0.2, - label = ~sub_name - ) |> - addMarkers( - lat = ~latitude, - lng = ~longitude, - popup = ~paste0( - "", address, "
", - "Sale Date: ", listed_date, "
", - "Price: ", price_fmt, "
", - "Sq Ft: ", sqft, "
", - "$/Sq Ft: ", ppsf_fmt - ) - ) |> - setView(lng = -82.362253, lat = 27.076199, zoom = 16) - }) - -# sales table ---- - output$sales_table <- renderDT({ - datatable( - sales |> select(listed_date, address, sqft, price_fmt, ppsf_fmt), - colnames = c("Date", "Address", "Sq Ft", "Price", "$/Sq Ft"), - rownames = FALSE, - options = list( - pageLength = 10, - searching = FALSE, - dom = 't' - ) - ) - }) - -# beach map ---- - output$beach_map <- renderLeaflet({ - leaflet() %>% - addProviderTiles("CartoDB.Voyager") %>% - setView(lng = -82.4603, lat = 27.0999, zoom = 12) %>% - addMarkers( - data = beaches, - lat = ~lat, - lng = ~lng, - popup = ~beach_name - ) - }) -} -# end server ---- +# Create the app +ui <- create_ui() +server <- create_server # Run the app shinyApp(ui, server) diff --git a/data-raw/update_owners.R b/data-raw/update_owners.R index 5d8a473..2a82a16 100644 --- a/data-raw/update_owners.R +++ b/data-raw/update_owners.R @@ -12,12 +12,12 @@ library(dplyr) library(stringr) library(sf) -options(timeout = 300) +# Load configuration +source("./R/config.R") -subdivisions <- c( - "8120", "8113", "8171", "8195", "8221", - "8163", "8240", "8159", "8149", "8110", "8254", "8215", "8143" -) +options(timeout = app_config$update_config$timeout_seconds) + +subdivisions <- app_config$update_config$subdivisions # load geometry lookup (static) ---- geometry_lookup <- readRDS("./data-raw/addresses/geometry_lookup.rds") @@ -73,6 +73,6 @@ latest_sale <- cat("Owners written:", nrow(owners), "\n") attr(owners, "last_sale_date") <- latest_sale$last_sale_date -saveRDS(owners, "./data/owners.rds") -cat("Saved to data/owners.rds\n") +saveRDS(owners, app_config$data_paths$owners) +cat("Saved to", app_config$data_paths$owners, "\n") cat("Most recent sale date:", format(latest_sale$last_sale_date, "%B %d, %Y"), "\n")