Files
stAndrews/app.R
Rob Wiederstein ec90f8236c
All checks were successful
Deploy stAndrews / deploy (push) Successful in 4s
Combine Beach and Resources into single Resources tab with segmented control
2026-03-09 17:28:14 -04:00

559 lines
17 KiB
R

# load libraries ----
library(shiny)
library(shinyMobile)
library(leaflet)
library(sf)
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")
# 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"),
tags$div(style = "display:none;", textInput("res_section", "", value = "beaches")),
f7Segment(
f7Button(inputId = "res_beaches", label = "Beaches"),
f7Button(inputId = "res_links", label = "Links")
),
##### beaches sub ----
conditionalPanel(
condition = "input.res_section == 'beaches'",
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)
)
)
),
##### links sub ----
conditionalPanel(
condition = "input.res_section == 'links'",
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")
)
)
)
)
### 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 ----
# 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
observeEvent(input$res_beaches, { updateTextInput(session, "res_section", value = "beaches") })
observeEvent(input$res_links, { updateTextInput(session, "res_section", value = "links") })
filteredSbdvn <- reactive({
if (is.null(input$sub_name) || input$sub_name == "All") {
return(sbdvn$max_sub_id)
} else {
return(
sbdvn %>%
filter(sub_name == input$sub_name) %>%
pull(max_sub_id)
)
}
})
filteredOwners <- reactiveVal(owners)
observeEvent(input$filterButton, {
filtered_owners <-
owners %>%
filter(subdivision %in% filteredSbdvn()) %>%
filter(
grepl(input$name, owner_1, ignore.case = TRUE) |
grepl(input$name, owner_2, ignore.case = TRUE)
) %>%
filter(grepl(input$location, location, ignore.case = TRUE))
filteredOwners(filtered_owners)
})
mean_lat <- reactive({
filteredOwners() %>%
st_coordinates() %>%
.[, "Y"] %>%
mean()
})
mean_lng <- reactive({
filteredOwners() %>%
st_coordinates() %>%
.[, "X"] %>%
mean()
})
output$map <- renderLeaflet({
if (!is.null(filteredOwners()) && nrow(filteredOwners()) > 0) {
leaflet() %>%
addProviderTiles("CartoDB.Voyager") %>%
addPolygons(
data = sbdvn,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2,
label = ~sub_name,
group = "Subdivisions"
) %>%
addMarkers(
data = filteredOwners(),
#color = ~ifelse(homestead == 1, "green", "red"),
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)
} else {
leaflet() %>%
addProviderTiles("CartoDB.Voyager") %>%
addPolygons(
data = sbdvn,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2,
label = ~sub_name,
group = "Subdivisions"
) %>%
setView(lng = -82.362253, lat = 27.076199, zoom = 16)
}
})
output$table <- renderDT({
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'
)
)
})
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(
"<b>", address, "</b><br>",
"Price: ", price_fmt, "<br>",
"Sq Ft: ", sqft, "<br>",
"$/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(
"<b>", address, "</b><br>",
"Sale Date: ", listed_date, "<br>",
"Price: ", price_fmt, "<br>",
"Sq Ft: ", sqft, "<br>",
"$/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 ----
# Run the app
shinyApp(ui, server)