refactor: modularize app into config, UI, and server components
Co-authored-by: aider (deepseek/deepseek-chat) <aider@aider.chat>
This commit is contained in:
371
R/app_server.R
Normal file
371
R/app_server.R
Normal file
@@ -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(
|
||||||
|
"<b>", address, "</b><br>",
|
||||||
|
"Price: ", price_fmt, "<br>",
|
||||||
|
"Sq Ft: ", sqft, "<br>",
|
||||||
|
"$/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(
|
||||||
|
"<b>", address, "</b><br>",
|
||||||
|
"Sale Date: ", listed_date, "<br>",
|
||||||
|
"Price: ", price_fmt, "<br>",
|
||||||
|
"Sq Ft: ", sqft, "<br>",
|
||||||
|
"$/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
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
232
R/app_ui.R
Normal file
232
R/app_ui.R
Normal file
@@ -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 ----
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
38
R/config.R
Normal file
38
R/config.R
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
608
app.R
608
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(shiny)
|
||||||
library(shinyMobile)
|
library(shinyMobile)
|
||||||
library(leaflet)
|
library(leaflet)
|
||||||
@@ -7,603 +10,16 @@ library(dplyr)
|
|||||||
library(leafpop)
|
library(leafpop)
|
||||||
library(DT)
|
library(DT)
|
||||||
|
|
||||||
# load data ----
|
# Load configuration
|
||||||
owners <- readRDS("./data/owners.rds")
|
source("./R/config.R")
|
||||||
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 ----
|
# Load UI and server components
|
||||||
ui <- f7Page(
|
source("./R/app_ui.R")
|
||||||
title = "St. Andrews",
|
source("./R/app_server.R")
|
||||||
## 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 ----
|
|
||||||
|
|
||||||
|
# Create the app
|
||||||
# define server ----
|
ui <- create_ui()
|
||||||
server <- function(input, output, session) {
|
server <- create_server
|
||||||
# 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(
|
|
||||||
"<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
|
# Run the app
|
||||||
shinyApp(ui, server)
|
shinyApp(ui, server)
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ library(dplyr)
|
|||||||
library(stringr)
|
library(stringr)
|
||||||
library(sf)
|
library(sf)
|
||||||
|
|
||||||
options(timeout = 300)
|
# Load configuration
|
||||||
|
source("./R/config.R")
|
||||||
|
|
||||||
subdivisions <- c(
|
options(timeout = app_config$update_config$timeout_seconds)
|
||||||
"8120", "8113", "8171", "8195", "8221",
|
|
||||||
"8163", "8240", "8159", "8149", "8110", "8254", "8215", "8143"
|
subdivisions <- app_config$update_config$subdivisions
|
||||||
)
|
|
||||||
|
|
||||||
# load geometry lookup (static) ----
|
# load geometry lookup (static) ----
|
||||||
geometry_lookup <- readRDS("./data-raw/addresses/geometry_lookup.rds")
|
geometry_lookup <- readRDS("./data-raw/addresses/geometry_lookup.rds")
|
||||||
@@ -73,6 +73,6 @@ latest_sale <-
|
|||||||
|
|
||||||
cat("Owners written:", nrow(owners), "\n")
|
cat("Owners written:", nrow(owners), "\n")
|
||||||
attr(owners, "last_sale_date") <- latest_sale$last_sale_date
|
attr(owners, "last_sale_date") <- latest_sale$last_sale_date
|
||||||
saveRDS(owners, "./data/owners.rds")
|
saveRDS(owners, app_config$data_paths$owners)
|
||||||
cat("Saved to data/owners.rds\n")
|
cat("Saved to", app_config$data_paths$owners, "\n")
|
||||||
cat("Most recent sale date:", format(latest_sale$last_sale_date, "%B %d, %Y"), "\n")
|
cat("Most recent sale date:", format(latest_sale$last_sale_date, "%B %d, %Y"), "\n")
|
||||||
|
|||||||
Reference in New Issue
Block a user