Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
39c2e29
created files for ambulance service area data, and made it work so th…
AtticusTarleton Jul 20, 2026
b5a3c9c
fixed a problem preventing mapping from working
AtticusTarleton Jul 21, 2026
47943da
fixing a gitignore issue
AtticusTarleton Jul 22, 2026
c0d2055
Merge branch 'main' into ambulance-data
AtticusTarleton Jul 22, 2026
0dbcdb1
Merge branch 'main' into ambulance-data
FWJK1 Aug 3, 2026
0459324
fixed ruff errors
AtticusTarleton Aug 3, 2026
e0d81e5
fixed a pathing error
AtticusTarleton Aug 3, 2026
34d738f
fixed prettier errors, and some sql errors (I believe in CDC.py
AtticusTarleton Aug 3, 2026
fc457e9
for some reason my ruff check was not finding the 2 spaces between fu…
AtticusTarleton Aug 3, 2026
72a87c6
added ruff to uv
AtticusTarleton Aug 3, 2026
869eb34
checking if the automatic ruff check on commits works
AtticusTarleton Aug 3, 2026
bbdb051
Merge branch 'main' into ambulance-data
AtticusTarleton Aug 10, 2026
a841b81
fixed a ruff error
AtticusTarleton Aug 10, 2026
802980c
small changes to meet ruff requirements
FWJK1 Aug 19, 2026
9b9733f
Merge branch 'main' into ambulance-data
FWJK1 Aug 19, 2026
f580311
added to main.py, slight refactoring to work with main, removed unrel…
FWJK1 Aug 19, 2026
020248c
added parquet
FWJK1 Aug 19, 2026
fc74974
added a legend to ambulance data and made a switch that sets a thicke…
AtticusTarleton Aug 19, 2026
3e6e3c4
prettier formatting
iansargent Aug 26, 2026
11db0d8
added next-env.d.ts to gitignore (per Next.js docs)
iansargent Aug 26, 2026
4441f26
also added to prettierignore
iansargent Aug 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ backend/logger/*.log
.env.test.local
.env.production.local

# Per Next.js docs, this one below should be in .gitignore
next-env.d.ts

# MacOS / IDE files
.DS_Store
*.swp
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
7 changes: 7 additions & 0 deletions backend/api/routes/get_routes/get_legend.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from query import (
get_soil_suit_legend,
get_ambulance_legend,
)

logger = logging.getLogger(__name__)
Expand All @@ -15,3 +16,9 @@
async def wastewater_soil_suit_legend():
data = get_soil_suit_legend()
return Response(content=data, media_type="application/json")


@router.get("/load/mapping/ambulance/ambulance_legend")
async def ambulance_legend():
data = get_ambulance_legend()
return Response(content=data, media_type="application/json")
2 changes: 2 additions & 0 deletions backend/api/routes/post_routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from .post_qcew import router as post_qcew_router
from .post_zoning import router as post_zoning_router
from .post_wastewater import router as post_wastewater_router
from .post_ambulance import router as post_ambulance_router

all_post_routers = [
post_zoning_router,
Expand All @@ -14,4 +15,5 @@
post_wastewater_router,
post_export_router,
post_cdc_router,
post_ambulance_router,
]
30 changes: 30 additions & 0 deletions backend/api/routes/post_routes/post_ambulance.py
Comment thread
AtticusTarleton marked this conversation as resolved.
Comment thread
AtticusTarleton marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from fastapi import APIRouter, Response

from api.core_functions import request_to_source, spec_to_source
from api.models import FilterRequest, FilterSpec
from query import (
get_ambulance_geojson,
get_ambulance_legend,
)

router = APIRouter()


@router.post("/load/mapping/ambulance/service_area")
async def ambulance_info_geojson(request: FilterRequest):
source = request_to_source(request, "ambulance_ambulance_info", "default")
data = get_ambulance_geojson([source])
return Response(content=data, media_type="application/json")


@router.post("/load/mapping/ambulance/service_area_new")
async def ambulance_info_geojson_new(specs: list[FilterSpec]):
sources = [spec_to_source(spec, "default") for spec in specs]
data = get_ambulance_geojson(sources)
return Response(content=data, media_type="application/json")


@router.get("/load/mapping/ambulance/ambulance_legend")
async def ambulance_legend():
data = get_ambulance_legend()
return Response(content=data, media_type="application/json")
8 changes: 8 additions & 0 deletions backend/api/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@
"Jurisdiction": "Municipal_Name",
"Town": "TownName"
}
},
"ambulance_ambulance_info": {
"join_key": "OBJECTID",
"join_type": "inner",
"columns": {
"Zip Code": "Zip_Code",
"City or Town": "City"
}
}
}
}
117 changes: 117 additions & 0 deletions backend/build/ambulance.py
Comment thread
AtticusTarleton marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""
**Author**:
Atticus Tarleton
**Created**:
2026-07-20
**Description**:
Build script to convert the ambulance service area files into SQL tables.
"""

import os
from pathlib import Path

import duckdb

_project_root = Path.cwd()
while not (_project_root / "api").exists():
_project_root = _project_root.parent
os.chdir(_project_root)
print(os.getcwd())

# globals
con = duckdb.connect()
proc_dir = Path("Data/_Processed/ambulance")
data_dir = Path("Data/ambulance/ambulance_service_areas.parquet")

# hardcoded specifics:
ambulance_info_cols = [
"OBJECTID",
"Serv_Name",
"Cert_Level",
"Address",
"Street_1",
"Street_2",
"City",
"State",
"Zip_Code",
"Total_Tran",
"Per_No_Tran",
"Re_Per_Tran",
"Cost_Per",
"Cost_Call",
]

ambulance_geom_cols = [
"OBJECTID",
"Shape__Area",
"Shape__Length",
"geometry",
]


# functions:
def load_ambulance_data():
con.execute("LOAD spatial")

con.execute(f"""--sql
CREATE OR REPLACE VIEW ambulance_service_areas AS
SELECT * FROM read_parquet('{data_dir}')
""")


def build_ambulance_info_table():
info_string = ", ".join(ambulance_info_cols)

con.execute(f"""--sql
CREATE OR REPLACE VIEW ambulance_info AS
SELECT {info_string}
FROM ambulance_service_areas
""")


def build_ambulance_geom_table():
geom_string = ", ".join(ambulance_geom_cols)

con.execute(f"""--sql
CREATE OR REPLACE VIEW ambulance_geom AS
SELECT {geom_string}
FROM ambulance_service_areas
""")


def build_ambulance_color_table():
con.execute("""--sql
CREATE TABLE ambulance_colors (
certification_level TEXT PRIMARY KEY,
hex_color TEXT NOT NULL,
rgba TEXT NOT NULL -- '[255,127,14,180]' as JSON-ish text
);

INSERT INTO ambulance_colors VALUES
('Paramedic', '#2ca02c', '[44, 160, 44, 180]'),
('Advanced EMT', '#ffcc00', '[255, 204, 0, 180]'),
('Paramedic - Critical Care Endorsement', '#fd7e14', '[253, 126, 20, 180]')
""")


def save_ambulance_tables():
load_ambulance_data()
build_ambulance_info_table()
build_ambulance_geom_table()
build_ambulance_color_table()

for table in ["ambulance_info", "ambulance_geom", "ambulance_colors"]:
con.execute(
f"COPY (SELECT * FROM {table}) TO '{proc_dir / f'{table}.parquet'}' "
)


## Putting everything together
def main():
proc_dir.mkdir(parents=True, exist_ok=True)
con.execute("LOAD spatial")
save_ambulance_tables()


if __name__ == "__main__":
main()
8 changes: 4 additions & 4 deletions backend/build/cdc.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def build_places(name: str, path: Path, indicators: str) -> None:
df = CON.execute(sql).df()
# Needed to get rid of df variable assignment to pass ruff linting check
if name == "county":
build_PCA_table(df)
pca_df = build_PCA_table(df)
pct_df = add_national_percentile(df) # df here is still national
df = df[df["StateAbbr"] == "VT"].copy()
df, edge_df = bin_measures(df, variable_col="Measure", value_col="Data_Value")
Expand All @@ -81,18 +81,18 @@ def build_places(name: str, path: Path, indicators: str) -> None:
CON.execute(f"""--sql
CREATE OR REPLACE TABLE {name}_places
AS SELECT *
FROM df
FROM {df}
""")
CON.execute(f"""--sql
CREATE OR REPLACE TABLE {name}_edges
AS SELECT *
FROM edge_df
FROM {edge_df}
""")
if name == "county":
CON.execute(f"""--sql
CREATE OR REPLACE TABLE {name}PcaData
AS SELECT *
FROM pca_df
FROM {pca_df}
""")


Expand Down
1 change: 0 additions & 1 deletion backend/build/consolidate.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"Data/_Processed/all_data-copy.duckdb",
]:
path = Path(__file__).parent.parent / db_path
path.touch(exist_ok=True)
db = duckdb.connect(str(db_path))
db.execute("INSTALL SPATIAL")
db.execute("LOAD SPATIAL")
Expand Down
3 changes: 2 additions & 1 deletion backend/build/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
Runs all the build scripts one after the other.
"""

from build import FIPS_data, acs5, cdc, wastewater, zoning
from build import FIPS_data, acs5, ambulance, cdc, wastewater, zoning


def main():
Expand All @@ -18,6 +18,7 @@ def main():
FIPS_data.main()
zoning.main()
wastewater.main()
ambulance.main()


if __name__ == "__main__":
Expand Down
1 change: 0 additions & 1 deletion backend/build/wastewater.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"""

# TODO: move the large SQL code to its own files rather than inside here
# TODO: also, need to go create the consolidated duckDB database

import os
from pathlib import Path
Expand Down
42 changes: 42 additions & 0 deletions backend/data_collection/ambulance.py
Comment thread
AtticusTarleton marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import pandas as pd
import requests
from io import BytesIO
from pyogrio import read_dataframe

AMBULANCE_SERVICE_AREA = "https://services1.arcgis.com/BkFxaEFNwHqX3tAw/arcgis/rest/services/FS_VCGI_OPENDATA_Emergency_AmbulanceServiceAreas_SP_v1/FeatureServer/0/query?outFields=*&where=1%3D1&f=geojson"
STORAGE_LOCATION = "../Data/ambulance"


# ---------------------------------------------------------------------------
# Ambulance API fetch
# ---------------------------------------------------------------------------

# Fetch ambulance data from goverment arcgis website (geojson files)


def fetch_service_areas() -> pd.DataFrame | None:
r = requests.get(AMBULANCE_SERVICE_AREA, timeout=30)
r.raise_for_status()
df = read_dataframe(BytesIO(r.content))
return df


# ---------------------------------------------------------------------------
# Main scrape runner
# ---------------------------------------------------------------------------


def run_ambulance_scrape() -> None:
"""
Fetch Wastewater data and save as parquet files.
"""
service_areas = fetch_service_areas()

if service_areas is not None:
service_areas.to_parquet(
f"{STORAGE_LOCATION}/ambulance_service_areas.parquet", index=False
)


if __name__ == "__main__":
run_ambulance_scrape()
Loading
Loading