Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
42 changes: 42 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# AI Coding Assistant Instructions for SEPTA Service Cuts Engagement Project

## Project Overview
This is a client-side web application that visualizes the impact of SEPTA bus service cuts on Philadelphia residents. Users enter their address or use geolocation to see how cuts affected bus routes in their Pennsylvania Senate district. The app uses Leaflet maps, Turf.js for geospatial analysis, and Census geocoding API.

## Architecture
- **Modular ES6**: Code split into `js/` modules (main.js, map.js, location.js, utils.js) with event-driven communication via a shared EventTarget bus.
- **Data Flow**: User location → geocoding → district lookup → filter/display route impacts.
- **No Build System**: Static files served directly; use ES modules and CDN libraries (Leaflet, Turf.js, Lodash).
- **Data Sources**: Pre-processed GeoJSON files in `data/` (routes, stops, districts) generated from R scripts.

## Key Conventions
- **Event Bus Pattern**: Use `events.dispatchEvent(new CustomEvent('eventName', { detail: data }))` for inter-module communication (e.g., `userLocationAcquired`, `districtData`).
- **Async Data Loading**: Fetch JSON with `await fetch('data/filename.json')` in `utils.js`.
- **Geospatial Ops**: Use Turf.js for point-in-polygon (district lookup) and masking.
- **Debounced Inputs**: Apply Lodash `_.debounce()` to address search for performance.
- **CORS Handling**: Route external API calls through `https://corsproxy.io/?url=` for Census geocoding.

## Developer Workflows
- **Linting**: Run `npm run js-lint` (ESLint) and `npm run css-lint` (Stylelint) before commits.
- **Data Processing**: Use R scripts in `r/` to transform raw CSV arrivals data into GeoJSON (e.g., `export_to_geojson.R`).
- **Local Development**: Open `index.html` in browser; no server needed for static assets.
- **Testing**: Manually test geolocation, address input, and map interactions across districts.

## Common Patterns
- **Map Styling**: Define Leaflet styles as functions returning objects (e.g., `standardStyle(feature)` in `map.js`).
- **District Overlay**: Use Turf `turf.mask()` to create inverted polygons for focus areas.
- **Error Handling**: Alert users for geolocation failures; log console errors for debugging.
- **Responsive Design**: CSS in `styles.css` handles sidebar/map layout.

## Integration Points
- **Leaflet Map**: Initialized in `map.js` with CartoDB basemap; add GeoJSON layers with custom styles.
- **Census API**: Geocode addresses via `https://geocoding.geo.census.gov/geocoder/locations/address` with Philadelphia bounds.
- **Turf.js**: Perform spatial queries like `turf.booleanPointInPolygon(point, polygon)`.

## File Structure Highlights
- `js/main.js`: Entry point, loads data, sets up event listeners.
- `js/location.js`: Handles geolocation and address search with debounced API calls.
- `js/map.js`: Manages Leaflet map, legend, and district masking.
- `data/`: Static GeoJSON (septa_routes.json, arrivals_and_stops.json, districts.json).
- `r/`: Data processing scripts for generating JSON from raw CSVs.</content>
<parameter name="filePath">/Users/chkim/Documents/Projects/classes/musa-javascript/septa-cuts-engagement/.github/copilot-instructions.md
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
node_modules/
.DS_Store
.DS_Store
*.csv
.Rhistory
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,19 @@
Add a readme for your engagement tool here. Include content overview, data citations, and any relevant technical details.
## Introduction

Due to a structural funding shortfall, SEPTA (the transit agency for the Philadelphia area) was forced to enact significant service cuts in the summer of 2025. The service cuts lasted two weeks, starting August 24. Though the cuts were reversed after the agency shifted funds from its capital budget to its operating budget, the underlying funding shortfall still remains, and similar cuts may return if a long-term funding agreement is not enacted by the Pennsylvania legislature.

## Purpose

This online tool is intended to help SEPTA riders (or potential SEPTA riders) understand what the day-to-day impacts of these service cuts were. The tool, based on actual service data rather than scheduled trips, allows users to select specific trips (for example, their commuting trips) to see how bus service to their own stop at particular times of day were impacted by the service cuts.

This tool focuses specifically on reduced bus service. Though the media and riders (understandably) paid much attention to the 32 eliminated and 19 shortened bus routes, as well as the threat of elimination for several Regional Rail lines, the degradation in service for routes which still existed—and continued to serve hundreds of thousands of riders—received less attention. This tool alleviates this information gap by quantifying the actual decrease in service as well as estimating the amount of time lost to riders because of the service cuts.

## Data and methods

The underlying data are real-time bus arrival data collected by SEPTA (and generously shared with the author). The data include arrival times for 41 bus routes which continued operations during the service cuts; these routes include high, medium, and low ridership routes, as well as a mix of urban and suburban routes. The time period of the data spans the week immediately before the cuts (August 17-23), as well as the first week of the cuts (August August 24-30).

The number of bus arrivals both before and during the cuts is calculated at each stop, separately for each route, each direction, day of the week (weekday vs weekend), and time of day. (The count is normalized by day of week, so that the values are comparable for weekdays and weekends. Further, any trip with a bus arrival count of zero either before or during the cuts is excluded from comparison.) Based on these counts, the percentage difference during the cuts vs before the cuts is calculated.

The estimates of waiting times are calculated by dividing the number of minutes in the time window by the number of arrivals, and dividing this value by two. For example, if 3 buses arrived at the stop during the morning rush period (7-10 am), the average waiting time would be 30 minutes (i.e., with a frequency of one bus per hour, if you arrive at a random time the expected value of of your waiting time would be 30 minutes). The difference in waiting time due to the cuts is simply the waiting time during the cuts minus the waiting time before the cuts.


113 changes: 113 additions & 0 deletions css/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
html {
font-size: 24px;
font-family: 'Gill Sans', sans-serif;
}

body {
padding: 0;
margin: 0;
}

.content {
display: flex;
flex-direction: row;
min-height: 100vh;
}

.map-container {
position: relative;
min-width: 0;
background-color: silver;
flex: 1 1 50%;
}

#map {
position: absolute;
inset: 0;
}

.marker-pin {
width: 30px;
height: 30px;
border-radius: 50% 50% 50% 0;
background: #000000;
position: absolute;
transform: rotate(-45deg);
left: 50%;
top: 50%;
margin: -15px 0 0 -15px;
}

.marker-pin::after {
content: '';
width: 24px;
height: 24px;
margin: 3px 0 0 3px;
background: #fff;
position: absolute;
border-radius: 50%;
}

.custom-div-icon i {
position: absolute;
width: 22px;
font-size: 22px;
left: 0;
right: 0;
margin: 10px auto;
text-align: center;
}

.custom-div-icon i.awesome {
margin: 12px auto;
font-size: 17px;
}

.sidebar {
position: relative;
background-color: #e9c46a;
padding: 16px;
display: flex;
flex-direction: column;
flex: 0 0 50%;
justify-content: space-around;
}

.sidebar-container {
background-color: white;
padding: 12px;
margin: 5px;
}

#trip-search-container {
display: flex;
flex-direction: column;
}

.input {
flex: 0 0 auto;
padding: 8px;
border: none;
border-radius: 4px;
margin-right: 10px;
}

.input label {
font-weight: bold;
}

.citations {
flex: 1 0 auto;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
font-size: 0.6em;
}







Loading