# 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 = "PrivateList",
title = "Private List",
icon = f7Icon("lock_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")
)
),
#### private list ----
f7Tab(
title = "Private List",
tabName = "PrivateList",
icon = f7Icon("lock_fill"),
f7Block(
tags$p(
style = "text-align:center; color:#ff9500; font-weight:bold;",
"Listings must be renewed every 45 days"
)
),
f7Card(
title = "Submit a Listing:",
divider = TRUE,
f7List(
inset = TRUE, dividers = TRUE, strong = TRUE,
f7Text("pl_address", "Address:", placeholder = "895 Chalmers Dr"),
f7Text("pl_price", "Price ($):", placeholder = "325000"),
f7Text("pl_sqft", "Sq Ft:", placeholder = "1800"),
f7Text("pl_name", "Your Name:", placeholder = "Jane Smith"),
f7Text("pl_email", "Email:", placeholder = "jane@email.com"),
f7Text("pl_cell", "Cell:", placeholder = "941-555-1234")
),
tags$br(),
f7Button("pl_submit", "Submit Listing", color = "blue"),
uiOutput("pl_message")
),
f7Card(
title = "Active Private Listings:",
divider = TRUE,
DTOutput("pl_table")
)
),
#### resources ----
f7Tab(
title = "Resources",
tabName = "Resources",
icon = f7Icon("hammer_fill"),
f7Segment(
f7Button(inputId = "res_beaches", label = "Beaches"),
f7Button(inputId = "res_links", label = "Links")
),
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 ----
# 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") })
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")
)
)
)
}
})
filteredSbdvn <- reactive({
if (is.null(input$sub_name) || input$sub_name == "All") {
return(unique(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)
}
)
# private listings ----
pl_path <- "./data/private_listings.csv"
pl_data <- reactivePoll(
intervalMillis = 5000,
session = session,
checkFunc = function() file.info(pl_path)$mtime,
valueFunc = function() {
df <- read.csv(pl_path, stringsAsFactors = FALSE)
if (nrow(df) == 0) return(df)
df$submitted_at <- as.POSIXct(df$submitted_at)
df[difftime(Sys.time(), df$submitted_at, units = "days") <= 45, ]
}
)
observeEvent(input$pl_submit, {
req(input$pl_address, input$pl_price, input$pl_name, input$pl_email, input$pl_cell)
new_row <- data.frame(
submitted_at = format(Sys.time(), "%Y-%m-%d %H:%M:%S"),
address = input$pl_address,
price = as.numeric(gsub("[^0-9.]", "", input$pl_price)),
sqft = as.numeric(gsub("[^0-9.]", "", input$pl_sqft)),
name = input$pl_name,
email = input$pl_email,
cell = input$pl_cell,
stringsAsFactors = FALSE
)
write.table(new_row, pl_path, sep = ",", append = TRUE,
col.names = FALSE, row.names = FALSE, quote = TRUE)
output$pl_message <- renderUI(
tags$p(style = "color:#4cd964;", "Listing submitted successfully!")
)
updateF7Text("pl_address", value = "")
updateF7Text("pl_price", value = "")
updateF7Text("pl_sqft", value = "")
updateF7Text("pl_name", value = "")
updateF7Text("pl_email", value = "")
updateF7Text("pl_cell", value = "")
})
output$pl_table <- renderDT({
df <- pl_data()
if (nrow(df) == 0) {
return(datatable(
data.frame(Message = "No active listings"),
rownames = FALSE, options = list(dom = 't', searching = FALSE)
))
}
datatable(
df |> dplyr::mutate(
price = scales::dollar(price),
sqft = formatC(sqft, format = "d", big.mark = ",")
) |> dplyr::select(address, price, sqft, name, email, cell),
colnames = c("Address", "Price", "Sq Ft", "Name", "Email", "Cell"),
rownames = FALSE,
options = list(pageLength = 25, searching = FALSE, dom = 't')
)
})
# 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 ----
# Run the app
shinyApp(ui, server)