Add deployment pipeline and clean up repo

- Add Dockerfile, docker-compose.yml, .dockerignore, .env (port 3842)
- Add Caddyfile.snippet for analytics gateway import pattern
- Add .gitea/workflows/deploy.yaml for act_runner SSH deploy
- Untrack sensitive/data files (SCPA xlsx, owners.rds)
- Add renv lockfile and infrastructure files
- Reorganize data-raw scripts and add SarasotaCounty boundary data
- Move www assets to www/images/, add docs PDFs
This commit is contained in:
2026-03-09 10:38:21 -04:00
parent 9dd0c7708d
commit 05e2aba34c
38 changed files with 6749 additions and 96 deletions

1
.Rprofile Normal file
View File

@@ -0,0 +1 @@
source("renv/activate.R")

19
.dockerignore Normal file
View File

@@ -0,0 +1,19 @@
renv/library/
renv/staging/
renv/local/
renv/python/
.Rproj.user/
.Rhistory
.RData
.DS_Store
.Rprofile
renv/activate.R
renv/settings.json
data-raw/
CLAUDE.md
TODO.md
README.md

2
.env Normal file
View File

@@ -0,0 +1,2 @@
PORT=3842
APP_NAME=stAndrews

View File

@@ -0,0 +1,33 @@
name: Deploy stAndrews
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
container:
image: debian:bookworm-slim
steps:
- name: Install SSH client
run: apt-get update -y && apt-get install -y --no-install-recommends openssh-client
- name: Deploy via SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
SERVER_IP: ${{ secrets.DEPLOY_SERVER_IP }}
SERVER_USER: ${{ secrets.DEPLOY_SERVER_USER }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts
ssh ${SERVER_USER}@${SERVER_IP} << 'EOF'
cd /data/projects/r/stAndrews
git pull
docker compose up -d --build
docker exec analytics-gateway caddy reload --config /etc/caddy/Caddyfile --adapter caddyfile
EOF

31
.gitignore vendored
View File

@@ -1,11 +1,32 @@
# R
.Rproj.user/
.Rhistory
.RData
rsconnect/
./data-raw/
.DS_Store
.Rproj.user
.Rdata
.httr-oauth
rsconnect/
# renv (library is rebuilt from renv.lock — do not commit)
renv/library/
renv/staging/
renv/local/
renv/python/
# macOS
.DS_Store
.quarto
# Quarto
.quarto/
# Raw data (large files, sensitive property data, not reproducible via code alone)
data-raw/property/
data-raw/addresses/
data-raw/geotagged_street_addresses.rds
data-raw/epa/
data-raw/uscb/
# Derived data (rebuilt from data-raw via data-raw/main.R)
data/owners.rds
data/venice.rds
data/venice_facts.rds
data/beaches.rds

191
CLAUDE.md Normal file
View File

@@ -0,0 +1,191 @@
# stAndrews — Claude Context
## Required reading
Before working on this project, read:
- `/home/rkw/.claude/CLAUDE.md` — infrastructure context (network, Docker, deployment)
- `/home/rkw/CLAUDE.md` — home directory layout
## What this project is
A Shiny mobile app (using `shinyMobile` / Framework7) that maps property owners
and community information for St. Andrews Park, Venice, Florida. 388 properties
across 14 subdivisions in Plantation Golf and Country Club.
**Entry point:** `app.R` (single-file Shiny app)
## Stack
- **UI:** `shinyMobile` (Framework7-based, iOS theme, dark mode)
- **Maps:** `leaflet` + `leafpop`
- **Tables:** `DT`
- **Spatial:** `sf`
- **Data wrangling:** `dplyr`
## Data
| File | Description | Source |
|------|-------------|--------|
| `data/owners.rds` | SF object — geocoded property owners (post-QGIS edit) | Built by `data-raw/main.R` |
| `data/plats/plats.shp` | Subdivision boundary polygons | Built by `data-raw/create_sbdv_plats.R` |
| `data/venice.rds` | Venice city boundary polygon | Built by `data-raw/create_venice_bndry.R` |
| `data/venice_facts.rds` | Venice demographic facts table | Built by `data-raw/main.R` or similar |
| `data/beaches.rds` | EPA beach monitoring locations | Built from `data-raw/epa/` |
**Raw inputs** (gitignored — large or sensitive):
- `data-raw/property/SCPA Public.xlsx` — Sarasota County Property Appraiser export
- `data-raw/addresses/owners_raw.gpkg` — geocoded points before QGIS editing
- `data-raw/addresses/owners_moved.gpkg` — points after manual QGIS adjustment (source of truth for `owners.rds`)
- `data-raw/geotagged_street_addresses.rds` — cached geocoding results (Google API)
**Rebuild pipeline:**
1. `data-raw/main.R` — geocodes addresses, exports to `owners_raw.gpkg`
2. Manual QGIS step — adjust duplicate-location points, save as `owners_moved.gpkg`
3. `data-raw/main.R` (continued) — reads QGIS output, writes `data/owners.rds`
4. `data-raw/create_sbdv_plats.R` — builds `data/plats/plats.shp`
5. `data-raw/create_venice_bndry.R` — builds `data/venice.rds`
## App tabs
| Tab | Content |
|-----|---------|
| About | Community description + photo |
| Venice | City boundary map + facts table |
| Beach | EPA beach locations map + helpful links |
| Owners | Searchable owner map + table (filter by name, address, subdivision) |
| Resources | Links to city services + PDF documents |
PDF documents are served from `www/docs/`.
## Running locally
Run inside the `rstudio` Docker container (see global CLAUDE.md):
```r
shiny::runApp("/home/rstudio/projects/r/stAndrews")
```
Or from RStudio with the project open: click **Run App**.
## Deployment
Deployed as a Shiny app. Path routing via the analytics gateway:
- Add a route to `~/docker/gateway/Caddyfile` on the analytics VM if not already present.
## Old app
Previously deployed to shinyapps.io at `https://rob-wiederstein.shinyapps.io/stAndrews/`
(account: `rob-wiederstein`, appId: 14173710). No longer maintained — app should be
archived/deleted from the shinyapps.io dashboard. The local `rsconnect/` directory
has been removed.
### Problems
**1. Geocoding was unreliable and required manual correction**
St. Andrews has three property types: standalone homes, villas (2- and 4-unit structures
where each side is a separate unit), and 8-unit buildings. The 8-unit buildings have unit
numbers (18) in the raw property records. Those unit numbers were stripped before
geocoding, so all 8 units in a building returned a single shared coordinate. Google's
geocoding was also only approximate — coordinates landed near addresses but not on top
of the actual structures. The result was that every point had to be manually reviewed
and dragged to its correct location in QGIS. This was labor-intensive and not repeatable.
**2. No update path**
The data pipeline was designed as a one-shot process with no way to refresh. Property
ownership changes hands regularly — the data should be updated roughly weekly. The
manual QGIS correction step makes this especially painful: any new or changed record
would require re-running geocoding and repeating the manual editing. There is no
mechanism to diff new records against existing ones or to carry forward previously
corrected coordinates.
### Geographic data flow
There are three independent geographic data flows, all ending in WGS84 (EPSG:4326):
**1. Owner points** (`data-raw/main.R`)
Raw property records from the Sarasota County Property Appraiser (Excel) are filtered
to the 13 St. Andrews subdivisions. Street addresses are parsed and assembled into
geocodable strings, then sent to the Google geocoding API via `tidygeocoder`. Results
are cached as `geotagged_street_addresses.rds` so the API isn't called twice. The
geocoded points are written to `owners_raw.gpkg`. Because many units share a building
address, they all land on the same coordinate — so the file is loaded into QGIS and
points are manually dragged to their actual locations. The adjusted file
(`owners_moved.gpkg`) is read back into R and saved as `data/owners.rds`.
**2. Subdivision boundary polygons** (`data-raw/create_sbdv_plats.R`)
A Sarasota County GIS shapefile (`data-raw/PlatBoundary/`) containing all county plat
boundaries is read in, reprojected to WGS84, and filtered to the same 13 subdivision
IDs. Subdivision names are cleaned up and the result is written to `data/plats/plats.shp`.
**3. Venice city boundary** (`data-raw/create_venice_bndry.R`)
A Sarasota County boundary shapefile (`data-raw/SarasotaCountyBoundary/`) is filtered
to the City of Venice (`municipali == "CV"`), keeping only the main polygon (acreage >
2500 to drop small outliers). It is simplified with `rmapshaper`, interior holes are
removed, reprojected to WGS84, and saved as `data/venice.rds`.
## New app — decisions
**Same folder/repo.** The UI in `app.R` is already good — tabs, maps, and layout don't
need to change. What needs replacing is entirely in `data-raw/`: the pipeline that
produces `data/owners.rds`. The app just consumes that file. Rebuild the pipeline,
keep the app.
**Feasibility hinges on the join.** The geometry side is straightforward — filter
building footprints to the subdivision boundary, compute centroids, done. The hard part
is linking those geometries to SCPA property records. Two approaches worth investigating:
- **Address join** — if the footprint layer carries situs addresses in the same format
as the SCPA records, a direct join works. If formatting differs, fuzzy matching may
be needed.
- **Account number join** — the SCPA assigns an account number to each property. It's
worth checking whether account numbers follow the *property* (stable, ideal join key)
or the *owner* (changes on sale, less useful). If account numbers are property-stable
and also appear in the footprint layer, this would be the cleanest and most reliable
join key — especially for multi-unit buildings where address disambiguation is messy.
Also needs confirming: whether the footprint polygons exist at the **unit level** or
only at the **building level**. If only building-level, the stacking problem from the
old pipeline reappears.
Data inspection will resolve all of this.
## New app — proposal
Instead of geocoding addresses, use the Sarasota County GIS building footprint layer,
which contains polygon outlines of every structure. Key advantages:
- **No geocoding API needed.** Building outlines are already precisely positioned over
actual structures. Deriving centroids from the polygons gives accurate, stable
coordinates for every unit — far better than approximate Google geocoding results.
- **Handles multi-unit buildings cleanly.** Each unit in an 8-unit building has its own
footprint polygon, so each gets its own distinct centroid. No stripping of unit numbers,
no stacking of points, no manual QGIS correction.
- **No new construction to worry about.** St. Andrews is a built-out community with no
active development, so the building footprint layer is effectively static.
**Proposed pipeline:**
1. Download the Sarasota County building footprint GIS layer.
2. Spatially filter it to the St. Andrews subdivision boundary (already have
`data/plats/plats.shp`).
3. Compute centroids for each building footprint polygon.
4. Join centroids to property records from the SCPA using address as the key.
5. Save the joined dataset as the owner points layer.
**Update path:**
Because the geometry is fixed (building centroids don't change), only the ownership
attributes need refreshing. The SCPA property records can be re-downloaded weekly and
re-joined to the stable centroid table. This makes weekly updates a simple scripted
operation with no manual steps.
## Notes
- Geocoding used Google API via `tidygeocoder`; results cached in `data-raw/geotagged_street_addresses.rds` to avoid re-calling the API.
- Point deduplication (multiple units at same address) was done manually in QGIS — not scripted. `owners_moved.gpkg` is the authoritative geocoded dataset.
- `data-raw/` is gitignored except for the shapefiles in `data-raw/PlatBoundary/` and `data-raw/SarasotaCountyBoundary/` which are committed.

5
Caddyfile.snippet Normal file
View File

@@ -0,0 +1,5 @@
# stAndrews — port 3842
redir /stAndrews /stAndrews/ 308
handle_path /stAndrews/* {
reverse_proxy 127.0.0.1:3842
}

47
Dockerfile Normal file
View File

@@ -0,0 +1,47 @@
FROM rocker/shiny:4.5.2
# 1. System dependencies (spatial stack required for sf)
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
cmake \
curl \
libabsl-dev \
libcurl4-openssl-dev \
libfontconfig1-dev \
libfreetype6-dev \
libfribidi-dev \
libgdal-dev \
libgeos-dev \
libharfbuzz-dev \
libicu-dev \
libjpeg-dev \
libpng-dev \
libproj-dev \
libssl-dev \
libtiff5-dev \
libudunits2-dev \
libxml2-dev \
pkg-config \
proj-bin \
proj-data \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /srv/shiny-server
# 2. Copy lockfile first for layer caching
COPY renv.lock .
# 3. Install packages into site-library (accessible to all users including shiny)
ENV RENV_PATHS_LIBRARY=/usr/local/lib/R/site-library
RUN R -q -e "install.packages('renv', repos='https://cloud.r-project.org')" \
&& R -q -e "renv::restore(library='/usr/local/lib/R/site-library', prompt=FALSE)"
# 4. Copy application code
COPY . .
# 5. Permissions
RUN chown -R shiny:shiny /srv/shiny-server
EXPOSE 3838
CMD ["/usr/bin/shiny-server"]

View File

@@ -1,22 +0,0 @@
library(dplyr, warn.conflicts = FALSE)
library(tidygeocoder)
# create a dataframe with addresses
some_addresses <- tibble::tribble(
~name, ~addr,
"White House", "1600 Pennsylvania Ave NW, Washington, DC",
"Transamerica Pyramid", "600 Montgomery St, San Francisco, CA 94111",
"Willis Tower", "233 S Wacker Dr, Chicago, IL 60606",
"Rob", "863 Tartan Dr, Venice, FL 34293"
)
# geocode the addresses
lat_longs <- some_addresses %>%
geocode(
addr,
method = 'google',
lat = latitude ,
long = longitude
)
#
#

View File

@@ -1,13 +1,45 @@
# St. Andrews
# St. Andrews Park
The project maps addresses of St. Andrews in Venice, Florida, to a map. The map is created with `leaflet`. St. Andrews is 388 separate properties in 14 subdivisions.
A Shiny mobile app for residents of St. Andrews Park, Venice, Florida.
## Data
## What it does
The data comes from the Sarasota County Property Appraiser for the property records and the Sarasota County GIS for the subdivision boundaries.
- Maps 388 property owners across 14 subdivisions
- Search owners by name, address, or subdivision
- Shows Venice city boundary and demographic facts
- Lists nearby EPA-monitored beach conditions
- Links to city services, county resources, and community documents
## Challenges
## Data sources
The property records are not geocoded, so the addresses need to be assigned a latitude and longitude. The records have two columns regarding address with the first portion being a street address like "123 Main St." and the second portion being a building and unit number. The second portion was inconsistent and only the first portion could be used.
- **Owners** — Sarasota County Property Appraiser (SCPA); updated weekly via `data-raw/update_owners.R`
- **Subdivision boundaries** — Sarasota County GIS plat layer
- **Venice boundary** — Sarasota County GIS municipal boundary layer
- **Beaches** — EPA beach monitoring data
This resulted in a building with 8 residents having 8 points fixed to the same location. This was fixed by manually moving each of the points within QGIS and exporting the data back to the project. Not great for reproducibility, but resulted in a much better and more accurate map.
## Weekly update
```r
source("./data-raw/update_owners.R")
```
Downloads fresh SCPA data, joins to stable geometry, overwrites `data/owners.rds`.
## Geometry
Owner point locations were geocoded via Google API, manually corrected in QGIS
(multi-unit buildings share a street address and required individual point placement),
and saved as `data-raw/addresses/owners_moved.gpkg`. This file is the stable geometry
source. Account numbers follow the property, so only ownership attributes need
refreshing weekly.
## Note: building footprints not used
Sarasota County publishes a building footprint GIS layer that could derive accurate
centroids without geocoding. However the footprint layer carries only a street number
(`buildingid`) with no street name — making a direct attribute join to SCPA records
ambiguous across multiple streets. A spatial join via street centerlines would resolve
this but adds complexity. Since accurate geometry already exists in `owners_moved.gpkg`
and St. Andrews is a built-out community with no new construction, the footprint
approach was deferred. It remains the right path if the app is ever extended to
additional subdivisions.

29
TODO.md Normal file
View File

@@ -0,0 +1,29 @@
# TODO
## App
- [ ] Display `last_sale_date` attribute from `owners.rds` somewhere in the UI
so users know how current the ownership data is
## Data
- [ ] Verify SCPA Public.xlsx column structure is stable across downloads
- [ ] Delete app from shinyapps.io (account: rob-wiederstein, appId: 14173710)
- [ ] Add cron job to run `update_owners.R` weekly inside the rstudio container:
`docker exec rstudio Rscript /home/rstudio/projects/r/stAndrews/data-raw/update_owners.R`
Note: app must reload `owners.rds` after refresh — either restart container or
make app reactive to file changes
## Deployment
Everything must be scripted from within the project folder. No manual steps
on the server outside of what the workflow handles.
- [ ] Create Dockerfile (follow veniceProp pattern — `rocker/shiny:4.5.2`)
- [ ] Create docker-compose.yml (bind to `127.0.0.1:<port>:3838`)
- [ ] Create `.gitea/workflows/deploy.yaml` that:
- Builds the Docker image
- Runs `docker compose up -d`
- SSHes into analytics VM and idempotently adds route to gateway Caddyfile
- Restarts the gateway container
- [ ] Push to Gitea — act_runner handles all future deploys automatically

344
app.R
View File

@@ -10,15 +10,19 @@ library(DT)
# load data ----
owners <- readRDS("./data/owners.rds")
sbdvn <- sf::st_read("./data/plats/plats.shp")
venice_bndry <- readRDS("./data/venice.rds")
venice_facts <- readRDS("./data/venice_facts.rds")
beaches <- readRDS("./data/beaches.rds")
# define ui
# 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$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;
@@ -44,76 +48,256 @@ ui <- f7Page(
}
"))
),
## options ----
options = list(
theme = "ios",
dark = TRUE,
pullToRefresh = TRUE
),
f7SingleLayout(
navbar = f7Navbar(title = "St. Andrews Park"),
toolbar = f7Toolbar(
position = "bottom",
f7Link(label = "SC-PA", href = "https://www.sc-pa.com/home/"),
f7Link(label = "GIS", href = "https://data-sarco.opendata.arcgis.com/")
),
f7Card(
title = "About",
divider = "TRUE",
img(src = "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")
)
),
f7Card(
title = "Owners:",
divider = TRUE,
raised = TRUE,
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"
## 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 = "Venice",
title = "Venice",
icon = f7Icon("map_pin")
),
f7PanelItem(
tabName = "Beach",
title = "Beach",
icon = f7Icon("sun_max_fill")
),
f7PanelItem(
tabName = "Owners",
title = "Owners",
icon = f7Icon("person_2_fill")
),
f7PanelItem(
tabName = "Resources",
title = "Resources",
icon = f7Icon("hammer_fill")
)
)
)
),
f7Card(
title = "Map:",
divider = TRUE,
leafletOutput("map")
### 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")
)
)
),
f7Card(
title = "Table:",
divider = TRUE,
DTOutput("table")
#### venice ----
f7Tab(
title = "Venice",
tabName = "Venice",
icon = f7Icon("map_pin"),
active = FALSE,
f7Card(
title = "Venice",
divider = "TRUE",
leafletOutput("venice_map")
),
f7Card(
title = "Facts",
divider = "TRUE",
DTOutput("venice_facts")
)
),
#### beaches ----
f7Tab(
title = "Beach",
tabName = "Beach",
icon = f7Icon("sun_max_fill"),
active = TRUE,
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
)
)
)
),
#### owners ----
f7Tab(
title = "Owners",
tabName = "Owners",
icon = f7Icon("person_2_fill"),
f7Card(
title = "Owners:",
divider = TRUE,
raised = TRUE,
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")
)
),
#### services ----
f7Tab(
title = "Resources",
tabName = "Resources",
icon = f7Icon("hammer_fill"),
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(); // Prevent the default link behavior
window.open($(this).attr('href'), '_blank'); // Open in a new tab
});
"
)
)
### end scripts ----
)
)
# end ui ----
# define server ----
server <- function(input, output) {
# update tabs depending on side panel
observeEvent(input$menu, {
updateF7Tabs(id = "tabs",
selected = input$menu)
})
filteredSbdvn <- reactive({
if (is.null(input$sub_name) || input$sub_name == "All") {
@@ -225,7 +409,47 @@ server <- function(input, output) {
)
)
})
# venice map ----
output$venice_map <- renderLeaflet({
leaflet() %>%
addProviderTiles("CartoDB.Voyager") %>%
setView(lng = -82.4313, lat = 27.1059, zoom = 12) %>%
addPolygons(
data = venice_bndry,
color = "red",
weight = 2,
opacity = 0.5,
fillOpacity = 0.2
)
})
# venice facts ----
output$venice_facts <- renderDT({
datatable(
venice_facts,
rownames = FALSE,
options = list(
pageLength = 10,
scrollX = TRUE,
searching = FALSE,
lengthMenu = c(5, 10, 25, 50),
dom = 'tpi'
)
)
})
# 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)
shinyApp(ui, server)

View File

@@ -0,0 +1 @@
UTF-8

View File

@@ -0,0 +1 @@
PROJCS["NAD_1983_HARN_StatePlane_Florida_West_FIPS_0902_Feet",GEOGCS["GCS_North_American_1983_HARN",DATUM["D_North_American_1983_HARN",SPHEROID["GRS_1980",6378137.0,298.257222101]],PRIMEM["Greenwich",0.0],UNIT["Degree",0.0174532925199433]],PROJECTION["Transverse_Mercator"],PARAMETER["False_Easting",656166.667],PARAMETER["False_Northing",0.0],PARAMETER["Central_Meridian",-82.0],PARAMETER["Scale_Factor",0.999941177],PARAMETER["Latitude_Of_Origin",24.3333333333333],UNIT["US survey foot",0.304800609601219]]

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?><MD_Metadata xmlns:gts="http://www.isotc211.org/2005/gts" xmlns:gco="http://www.isotc211.org/2005/gco" xmlns:xalan="http://xml.apache.org/xalan" xmlns:srv="http://www.isotc211.org/2005/srv" xmlns:gmx="http://www.isotc211.org/2005/gmx" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gml="http://www.opengis.net/gml" xmlns="http://www.isotc211.org/2005/gmd">
<characterSet>
<MD_CharacterSetCode codeList="http://www.isotc211.org/2005/resources/Codelist/gmxCodelists.xml#MD_CharacterSetCode" codeListValue="utf8" codeSpace="ISOTC211/19115">utf8</MD_CharacterSetCode>
</characterSet>
<hierarchyLevel>
<MD_ScopeCode codeList="http://www.isotc211.org/2005/resources/Codelist/gmxCodelists.xml#MD_ScopeCode" codeListValue="dataset" codeSpace="ISOTC211/19115">dataset</MD_ScopeCode>
</hierarchyLevel>
<hierarchyLevelName>
<gco:CharacterString>dataset</gco:CharacterString>
</hierarchyLevelName>
<contact gco:nilReason="missing"/>
<dateStamp gco:nilReason="missing"/>
<metadataStandardName>
<gco:CharacterString>ISO 19139 Geographic Information - Metadata - Implementation Specification</gco:CharacterString>
</metadataStandardName>
<metadataStandardVersion>
<gco:CharacterString>2007</gco:CharacterString>
</metadataStandardVersion>
<identificationInfo>
<MD_DataIdentification>
<citation>
<CI_Citation>
<title>
<gco:CharacterString>SarasotaCountyBoundary</gco:CharacterString>
</title>
<date gco:nilReason="missing"/>
</CI_Citation>
</citation>
<abstract>
<gco:CharacterString>The Sarasota County Boundary layer contains both Sarasota County and municipal boundary lines and areas.</gco:CharacterString>
</abstract>
<purpose>
<gco:CharacterString>The Sarasota County Boundary layer contains both Sarasota County and municipal boundary lines and areas.</gco:CharacterString>
</purpose>
<descriptiveKeywords>
<MD_Keywords>
<keyword>
<gco:CharacterString>Cadastral</gco:CharacterString>
</keyword>
<keyword>
<gco:CharacterString>Planning</gco:CharacterString>
</keyword>
</MD_Keywords>
</descriptiveKeywords>
<language gco:nilReason="missing"/>
<characterSet>
<MD_CharacterSetCode codeList="http://www.isotc211.org/2005/resources/Codelist/gmxCodelists.xml#MD_CharacterSetCode" codeListValue="utf8" codeSpace="ISOTC211/19115">utf8</MD_CharacterSetCode>
</characterSet>
<extent>
<EX_Extent>
<geographicElement>
<EX_GeographicBoundingBox>
<westBoundLongitude>
<gco:Decimal>-82.6425729822934</gco:Decimal>
</westBoundLongitude>
<eastBoundLongitude>
<gco:Decimal>-82.0565543313639</gco:Decimal>
</eastBoundLongitude>
<southBoundLatitude>
<gco:Decimal>26.9440430628277</gco:Decimal>
</southBoundLatitude>
<northBoundLatitude>
<gco:Decimal>27.391160931026</gco:Decimal>
</northBoundLatitude>
</EX_GeographicBoundingBox>
</geographicElement>
</EX_Extent>
</extent>
</MD_DataIdentification>
</identificationInfo>
</MD_Metadata>

View File

@@ -0,0 +1,24 @@
# create_geometry_lookup.R
# One-time script. Extracts account_number -> geometry from the manually
# corrected QGIS file and saves as a stable lookup table.
# Re-run only if geometry ever needs to be corrected.
# Output: data-raw/addresses/geometry_lookup.rds
library(sf)
library(dplyr)
raw <- st_read(
"./data-raw/addresses/owners_moved.gpkg",
layer = "owners_raw",
quiet = TRUE
)
cat("Columns:", paste(names(raw), collapse = ", "), "\n")
lookup <- raw |>
select(account_number)
cat("Lookup rows:", nrow(lookup), "\n")
cat("Any duplicate account numbers:", anyDuplicated(lookup$account_number) > 0, "\n")
saveRDS(lookup, "./data-raw/addresses/geometry_lookup.rds")
cat("Saved to data-raw/addresses/geometry_lookup.rds\n")

View File

@@ -0,0 +1,15 @@
library(rmapshaper)
library(dplyr)
library(sf)
library(leaflet)
library(sfheaders)
df <- st_read("./data-raw/SarasotaCountyBoundary/") %>%
filter(municipali == "CV") %>%
filter(acreage > 2500) %>%
ms_simplify(keep = .025) %>%
mutate(geometry = sfheaders::sf_remove_holes(geometry)) %>%
sf::st_transform(crs = 4326) %>%
mutate(label = "City of Venice", .before = boundarycl) %>%
saveRDS(., file = "./data/venice.rds")

View File

@@ -4,7 +4,7 @@ library("readxl")
library("janitor")
library("dplyr")
library("tidyr")
library("tidygeotag")
library("tidygeocoder")
library("sf")
# choose columns ---

Binary file not shown.

View File

@@ -0,0 +1,56 @@
# create_building_footprints.R
# Pull Sarasota County building footprints clipped to St. Andrews boundary.
# Source: https://ags3.scgov.net/server/rest/services/Hosted/BuildingFootprint/FeatureServer/0
# Output: data-raw/sarco/building_footprints/building_footprints.gpkg
# No esri2sf needed — queries ArcGIS REST API directly via GeoJSON URL.
library(sf)
library(dplyr)
# load st. andrews subdivision boundary ----
plats <- st_read("./data/plats/plats.shp", quiet = TRUE) |> st_transform(4326)
boundary <- st_union(plats)
bb <- st_bbox(boundary)
# build arcgis rest query url ----
base_url <- "https://ags3.scgov.net/server/rest/services/Hosted/BuildingFootprint/FeatureServer/0/query"
geometry <- paste(bb["xmin"], bb["ymin"], bb["xmax"], bb["ymax"], sep = ",")
params <- paste0(
"?where=1=1",
"&geometry=", geometry,
"&geometryType=esriGeometryEnvelope",
"&inSR=4326",
"&spatialRel=esriSpatialRelIntersects",
"&outFields=*",
"&returnGeometry=true",
"&f=geojson"
)
url <- paste0(base_url, params)
cat("URL:\n", url, "\n\n")
# fetch ----
cat("Querying feature service...\n")
footprints_raw <- st_read(url, quiet = TRUE)
cat("Raw rows:", nrow(footprints_raw), "\n")
cat("Raw fields:", paste(names(footprints_raw), collapse = ", "), "\n\n")
# clip to exact subdivision boundary ----
footprints <- footprints_raw |>
st_transform(4326) |>
st_filter(boundary)
# inspect ----
cat("Rows:", nrow(footprints), "\n")
cat("\nFields:\n")
print(names(footprints))
cat("\nSample rows:\n")
print(head(st_drop_geometry(footprints)))
# save ----
st_write(
footprints,
"./data-raw/sarco/building_footprints/building_footprints.gpkg",
delete_dsn = TRUE
)
cat("\nSaved to data-raw/sarco/building_footprints/building_footprints.gpkg\n")

82
data-raw/update_owners.R Normal file
View File

@@ -0,0 +1,82 @@
# update_owners.R
# Weekly update script. Reads fresh SCPA property data, joins to stable
# geometry lookup by account_number, saves data/owners.rds.
# Only the SCPA xlsx needs to be replaced to refresh ownership data.
# Input: data-raw/property/SCPA Public.xlsx (replace weekly)
# data-raw/addresses/geometry_lookup.rds (static)
# Output: data/owners.rds
library(readxl)
library(janitor)
library(dplyr)
library(stringr)
library(sf)
subdivisions <- c(
"8120", "8113", "8171", "8195", "8221",
"8163", "8240", "8159", "8149", "8110", "8254", "8215", "8143"
)
# load geometry lookup (static) ----
geometry_lookup <- readRDS("./data-raw/addresses/geometry_lookup.rds")
# download fresh scpa data ----
download.file(
url = "https://www.sc-pa.com/downloads/SCPA%20Public.xlsx",
destfile = "./data-raw/property/SCPA Public.xlsx",
mode = "wb"
)
# load and clean scpa data ----
owners_raw <-
readxl::read_xlsx(
path = "./data-raw/property/SCPA Public.xlsx",
n_max = Inf,
.name_repair = ~janitor::make_clean_names(.x)
) |>
filter(subdivision %in% subdivisions) |>
rename(
situs_address = situs_address_property_address,
homestead = homestead_exemption_yes_or_no
) |>
filter(!is.na(situs_address)) |>
filter(!grepl("^0", situs_address)) |>
mutate(
# extract clean street address (before multiple spaces / unit suffix)
label = str_trim(str_extract(situs_address, "^\\d+\\s+\\S+\\s+\\S+")),
location = paste0(label, ", Venice FL")
) |>
select(account_number, owner_1, owner_2, subdivision, homestead, label, location)
# join to geometry ----
owners <- owners_raw |>
inner_join(geometry_lookup, by = "account_number") |>
st_as_sf(sf_column_name = "geom")
# report any unmatched records ----
n_unmatched <- nrow(owners_raw) - nrow(owners)
if (n_unmatched > 0) {
cat("WARNING:", n_unmatched, "records had no matching geometry and were dropped.\n")
missing <- anti_join(owners_raw, st_drop_geometry(geometry_lookup), by = "account_number")
print(missing)
}
# report most recent sale date in st. andrews ----
latest_sale <-
readxl::read_xlsx(
path = "./data-raw/property/SCPA Public.xlsx",
n_max = Inf,
.name_repair = ~janitor::make_clean_names(.x)
) |>
filter(subdivision %in% subdivisions) |>
select(account_number, owner_1, contains("sale")) |>
filter(!is.na(account_number)) |>
mutate(last_sale_date = as.Date(last_sale_date, format = "%m/%d/%Y")) |>
arrange(desc(last_sale_date)) |>
head(1)
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")
cat("Most recent sale date:", format(latest_sale$last_sale_date, "%B %d, %Y"), "\n")

Binary file not shown.

18
docker-compose.yml Normal file
View File

@@ -0,0 +1,18 @@
services:
shiny:
build: .
container_name: standrews_shiny
restart: always
ports:
- "127.0.0.1:${PORT}:3838"
environment:
- SHINY_LOG_STDERR=1
volumes:
- .:/srv/shiny-server
# Prevents local renv library from shadowing the container's library
- /srv/shiny-server/renv/library
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3838/"]
interval: 1m
timeout: 10s
retries: 3

4357
renv.lock Normal file

File diff suppressed because one or more lines are too long

7
renv/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
library/
local/
cellar/
lock/
python/
sandbox/
staging/

1419
renv/activate.R Normal file

File diff suppressed because it is too large Load Diff

20
renv/settings.json Normal file
View File

@@ -0,0 +1,20 @@
{
"bioconductor.version": null,
"external.libraries": [],
"ignored.packages": [],
"package.dependency.fields": [
"Imports",
"Depends",
"LinkingTo"
],
"ppm.enabled": null,
"ppm.ignored.urls": [],
"r.version": null,
"snapshot.dev": false,
"snapshot.type": "implicit",
"use.cache": true,
"vcs.ignore.cellar": true,
"vcs.ignore.library": true,
"vcs.ignore.local": true,
"vcs.manage.ignores": true
}

View File

@@ -1,5 +1,5 @@
Version: 1.0
ProjectId: c59d4069-2369-4870-a54f-b22df07f3e3e
ProjectId: ab728b89-b094-4c83-8cc4-50bda69048f8
RestoreWorkspace: Default
SaveWorkspace: Default

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB