Skip to content

Commit b4f7600

Browse files
Merge pull request #121 from OneBusAway/claude
Add support for launching the server from a local GTFS zip file
2 parents 87d9aae + 4c872e6 commit b4f7600

File tree

12 files changed

+482
-34
lines changed

12 files changed

+482
-34
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
ci-bundle
22
bundle
33
.DS_Store
4+
.claude
5+
example-local-gtfs/*.zip

CLAUDE.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
This repository contains Docker images for running OneBusAway Application Suite v2, a transit data platform. The system consists of three main components:
8+
9+
1. **Bundler Service**: Processes GTFS data and creates transit data bundles
10+
2. **OBA App Service**: Runs the OneBusAway API and transit data federation webapps
11+
3. **Database Service**: MySQL (default) or PostgreSQL for storing application data
12+
13+
## Common Commands
14+
15+
### Building and Running
16+
17+
```bash
18+
# Build the app server
19+
docker compose build oba_app
20+
21+
# Build a bundle with custom GTFS
22+
GTFS_URL=https://example.com/gtfs.zip docker compose up oba_bundler
23+
24+
# Build with default test data (Unitrans)
25+
docker compose up oba_bundler
26+
27+
# Run the OBA server
28+
docker compose up oba_app
29+
30+
# Run validation tests
31+
./bin/validate.sh
32+
33+
# Clean up
34+
docker compose down -v
35+
```
36+
37+
### Testing
38+
39+
```bash
40+
# Run validation script to test API endpoints
41+
./bin/validate.sh
42+
43+
# Build and test Docker images locally
44+
docker compose build
45+
docker compose up
46+
```
47+
48+
## Architecture
49+
50+
### Directory Structure
51+
52+
- `/bundler/`: Docker setup for building transit data bundles from GTFS feeds
53+
- Uses Maven to fetch OneBusAway dependencies
54+
- Includes gtfstidy for cleaning/optimizing GTFS data
55+
- Outputs to `/bundle/` directory
56+
57+
- `/oba/`: Docker setup for the OneBusAway application server
58+
- Runs on Tomcat 8.5 with Java 11
59+
- Template-based configuration for database connections
60+
- Supports GTFS-RT feeds
61+
- Includes Prometheus JMX exporter for monitoring
62+
63+
- `/bundle/`: Shared volume containing processed transit data
64+
- Contains serialized Java objects (.obj files)
65+
- Lucene search indices
66+
- Processed GTFS data
67+
68+
### Key Technologies
69+
70+
- **Build System**: Maven-based Java project
71+
- **OneBusAway Version**: v2.6.0 (configurable via OBA_VERSION)
72+
- **Runtime**: Tomcat 8.5.100 with JDK 11
73+
- **Databases**: MySQL 8.0 or PostgreSQL 16
74+
- **GTFS Processing**: gtfstidy (Go-based optimizer)
75+
- **Template Engine**: Custom Handlebars renderer (Go)
76+
77+
### Environment Variables
78+
79+
Database configuration:
80+
- `JDBC_URL`, `JDBC_DRIVER`, `JDBC_USER`, `JDBC_PASSWORD`
81+
82+
GTFS configuration:
83+
- `GTFS_URL`: URL to GTFS zip file
84+
- `TZ`: Timezone for the transit agency
85+
86+
GTFS-RT configuration:
87+
- `TRIP_UPDATES_URL`, `VEHICLE_POSITIONS_URL`, `ALERTS_URL`
88+
- `REFRESH_INTERVAL`: Update frequency in seconds
89+
- `AGENCY_ID`: Transit agency identifier
90+
- `FEED_API_KEY`, `FEED_API_VALUE`: Authentication headers
91+
92+
### API Endpoints
93+
94+
When running locally:
95+
- API webapp: http://localhost:8080/
96+
- Example: http://localhost:8080/api/where/agencies-with-coverage.json?key=TEST
97+
- Transit data federation: http://localhost:8080/onebusaway-transit-data-federation-webapp
98+
99+
### Docker Images
100+
101+
Published to Docker Hub:
102+
- `opentransitsoftwarefoundation/onebusaway-bundle-builder`
103+
- `opentransitsoftwarefoundation/onebusaway-api-webapp`
104+
105+
Multi-architecture support: x86_64, ARM64

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ You will then have two web apps available:
6363

6464
When done using this web server, you can use the shell-standard `^C` to exit out and turn it off. If issues persist across runs, you can try using `docker compose down -v` and then `docker compose up oba_app` to refresh the Docker containers and services.
6565

66+
### Using local GTFS files
67+
68+
If you have a local GTFS file instead of downloading from a URL, see the [`example-local-gtfs/`](example-local-gtfs/) directory for a complete example that demonstrates how to build bundles using local GTFS files.
69+
6670
### Inspecting the database
6771

6872
The Docker Compose database service should remain up after a call of `docker compose up oba_app`. Otherwise, you can always invoke it using `docker compose up oba_database`.

bin/validate.sh

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,82 @@ else
99
exit 1
1010
fi
1111

12-
output=$(curl -s "http://localhost:8080/api/where/agencies-with-coverage.json?key=test" | jq '.data.list[0].agencyId')
12+
# Get the first agency from agencies-with-coverage
13+
agency_response=$(curl -s "http://localhost:8080/api/where/agencies-with-coverage.json?key=test")
14+
agency_count=$(echo "$agency_response" | jq '.data.list | length')
1315

14-
if [[ ! -z "$output" && "$output" == "\"unitrans\"" ]]; then
15-
echo "agencies-with-coverage.json endpoint works."
16+
if [[ "$agency_count" -gt 0 ]]; then
17+
echo "agencies-with-coverage.json endpoint works (found $agency_count agencies)."
18+
AGENCY_ID=$(echo "$agency_response" | jq -r '.data.list[0].agencyId')
19+
echo "Using agency: $AGENCY_ID"
1620
else
17-
echo "Error: agencies-with-coverage.json endpoint is not working: $output"
21+
echo "Error: agencies-with-coverage.json endpoint is not working or no agencies found: $agency_count"
1822
exit 1
1923
fi
2024

21-
output=$(curl -s "http://localhost:8080/api/where/routes-for-agency/unitrans.json?key=test" | jq '.data.list | length')
22-
if [[ $output -gt 10 ]]; then
23-
echo "routes-for-agency/unitrans.json endpoint works."
25+
# Get routes for the agency
26+
routes_response=$(curl -s "http://localhost:8080/api/where/routes-for-agency/${AGENCY_ID}.json?key=test")
27+
route_count=$(echo "$routes_response" | jq '.data.list | length')
28+
if [[ "$route_count" -gt 0 ]]; then
29+
echo "routes-for-agency/${AGENCY_ID}.json endpoint works (found $route_count routes)."
30+
ROUTE_ID=$(echo "$routes_response" | jq -r '.data.list[0].id')
31+
echo "Using route: $ROUTE_ID"
2432
else
25-
echo "Error: routes-for-agency/unitrans.json is not working: $output"
33+
echo "Error: routes-for-agency/${AGENCY_ID}.json is not working or no routes found: $route_count"
2634
exit 1
2735
fi
2836

29-
output=$(curl -s "http://localhost:8080/api/where/stops-for-route/unitrans_C.json?key=test" | jq '.data.entry.routeId')
30-
if [[ ! -z "$output" && "$output" == "\"unitrans_C\"" ]]; then
31-
echo "stops-for-route/unitrans_C.json endpoint works."
37+
# Get stops for the route
38+
stops_response=$(curl -s "http://localhost:8080/api/where/stops-for-route/${ROUTE_ID}.json?key=test")
39+
route_id_check=$(echo "$stops_response" | jq -r '.data.entry.routeId')
40+
if [[ ! -z "$route_id_check" && "$route_id_check" == "$ROUTE_ID" ]]; then
41+
echo "stops-for-route/${ROUTE_ID}.json endpoint works."
42+
STOP_ID=$(echo "$stops_response" | jq -r '.data.entry.stopIds[0]')
43+
echo "Using stop: $STOP_ID"
3244
else
33-
echo "Error: stops-for-route/unitrans_C.json endpoint is not working: $output"
45+
echo "Error: stops-for-route/${ROUTE_ID}.json endpoint is not working: $route_id_check"
3446
exit 1
3547
fi
3648

37-
output=$(curl -s "http://localhost:8080/api/where/stop/unitrans_22182.json?key=test" | jq '.data.entry.code')
38-
if [[ ! -z "$output" && "$output" == "\"22182\"" ]]; then
39-
echo "stop/unitrans_22182.json endpoint works."
49+
# Get stop details
50+
stop_response=$(curl -s "http://localhost:8080/api/where/stop/${STOP_ID}.json?key=test")
51+
stop_id_check=$(echo "$stop_response" | jq -r '.data.entry.id')
52+
if [[ ! -z "$stop_id_check" && "$stop_id_check" == "$STOP_ID" ]]; then
53+
echo "stop/${STOP_ID}.json endpoint works."
54+
# Extract coordinates for stops-for-location test
55+
STOP_LAT=$(echo "$stop_response" | jq -r '.data.entry.lat')
56+
STOP_LON=$(echo "$stop_response" | jq -r '.data.entry.lon')
57+
echo "Using coordinates: $STOP_LAT, $STOP_LON"
4058
else
41-
echo "Error: stop/unitrans_22182.json endpoint is not working: $output"
59+
echo "Error: stop/${STOP_ID}.json endpoint is not working: $stop_id_check"
4260
exit 1
4361
fi
4462

45-
output=$(curl -s "http://localhost:8080/api/where/stops-for-location.json?lat=38.555308&lon=-121.735991&key=test" | jq '.data.outOfRange')
46-
if [[ ! -z "$output" && "$output" == "false" ]]; then
47-
echo "stops-for-location/unitrans_false.json endpoint works."
63+
# Test stops-for-location using coordinates from the stop
64+
LOCATION_URL="http://localhost:8080/api/where/stops-for-location.json?lat=${STOP_LAT}&lon=${STOP_LON}&key=test"
65+
location_response=$(curl -s "$LOCATION_URL")
66+
out_of_range=$(echo "$location_response" | jq '.data.outOfRange')
67+
stops_found=$(echo "$location_response" | jq '.data.list | length')
68+
if [[ ! -z "$out_of_range" && "$out_of_range" == "false" && "$stops_found" -gt 0 ]]; then
69+
echo "stops-for-location.json endpoint works (found $stops_found stops)."
4870
else
49-
echo "Error: stops-for-location/unitrans_false.json endpoint is not working: $output"
71+
echo "Error: stops-for-location.json endpoint is not working: outOfRange=$out_of_range, stops=$stops_found"
72+
echo "URL: $LOCATION_URL"
73+
echo "Response: $location_response"
5074
exit 1
5175
fi
5276

53-
# todo: add support for arrivals-and-departures-for-stop endpoint.
54-
# however, it doesn't seem that the unitrans_22182 stop has arrivals and departures on the weekend, so we'll need
55-
# something else to test with. However, for now, this is still a great step forward.
77+
# Test arrivals-and-departures-for-stop endpoint
78+
arrivals_response=$(curl -s "http://localhost:8080/api/where/arrivals-and-departures-for-stop/${STOP_ID}.json?key=test")
79+
arrivals_stop_id=$(echo "$arrivals_response" | jq -r '.data.entry.stopId')
80+
arrivals_count=$(echo "$arrivals_response" | jq '.data.entry.arrivalsAndDepartures | length // 0')
81+
82+
if [[ "$arrivals_stop_id" == "$STOP_ID" ]]; then
83+
if [[ "$arrivals_count" -gt 0 ]]; then
84+
echo "arrivals-and-departures-for-stop/${STOP_ID}.json endpoint works (found $arrivals_count arrivals/departures)."
85+
else
86+
echo "arrivals-and-departures-for-stop/${STOP_ID}.json endpoint works but no arrivals/departures at this time."
87+
fi
88+
else
89+
echo "Warning: arrivals-and-departures-for-stop/${STOP_ID}.json endpoint may not be working correctly."
90+
fi

bundler/build_bundle.sh

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,14 @@
1616
# limitations under the License.
1717
#
1818

19-
if [ -z "$GTFS_URL" ]; then
20-
echo "GTFS_URL is not set"
19+
# Check that either GTFS_URL or GTFS_ZIP_FILENAME is set, but not both
20+
if [ -n "$GTFS_URL" ] && [ -n "$GTFS_ZIP_FILENAME" ]; then
21+
echo "Error: Both GTFS_URL and GTFS_ZIP_FILENAME are set. Please provide only one."
22+
exit 1
23+
fi
24+
25+
if [ -z "$GTFS_URL" ] && [ -z "$GTFS_ZIP_FILENAME" ]; then
26+
echo "Error: Neither GTFS_URL nor GTFS_ZIP_FILENAME is set. Please provide one."
2127
exit 1
2228
fi
2329

@@ -35,17 +41,33 @@ TDF_BUILDER_JAR=${TDF_BUILDER_JAR:-/oba/libs/onebusaway-transit-data-federation-
3541
# -D: drop erroneous entries from feed
3642
GTFS_TIDY_ARGS=${GTFS_TIDY_ARGS:-OscRCSmeD}
3743

38-
GTFS_ZIP_FILENAME="gtfs_pristine.zip"
44+
# Set default filename if using GTFS_URL
45+
if [ -n "$GTFS_URL" ]; then
46+
GTFS_ZIP_FILENAME="gtfs_pristine.zip"
47+
fi
3948

4049
echo "OBA Bundle Builder Starting"
41-
echo "GTFS_URL: $GTFS_URL"
50+
if [ -n "$GTFS_URL" ]; then
51+
echo "GTFS_URL: $GTFS_URL"
52+
else
53+
echo "GTFS_ZIP_FILENAME: $GTFS_ZIP_FILENAME"
54+
fi
4255
echo "OBA Version: $OBA_VERSION"
4356
echo "GTFS Tidy Args: $GTFS_TIDY_ARGS"
4457
echo "TDF_BUILDER_JAR: $TDF_BUILDER_JAR"
4558

4659
cd /bundle
4760

48-
wget -O ${GTFS_ZIP_FILENAME} ${GTFS_URL}
61+
# Download GTFS file if URL is provided, otherwise use local file
62+
if [ -n "$GTFS_URL" ]; then
63+
wget -O ${GTFS_ZIP_FILENAME} ${GTFS_URL}
64+
else
65+
# Check if the local file exists
66+
if [ ! -f "$GTFS_ZIP_FILENAME" ]; then
67+
echo "Error: GTFS file not found: $GTFS_ZIP_FILENAME"
68+
exit 1
69+
fi
70+
fi
4971

5072
gtfstidy -${GTFS_TIDY_ARGS} ${GTFS_ZIP_FILENAME}
5173

example-local-gtfs/.dockerignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Ignore everything except GTFS files and Docker-related files
2+
*
3+
!*.zip
4+
!*.gtfs
5+
!Dockerfile
6+
!docker-entrypoint.sh
7+
8+
# But still ignore these even if they're zip files
9+
!backup*.zip
10+
!old*.zip

example-local-gtfs/Dockerfile

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Example Dockerfile for using local GTFS files with OneBusAway
2+
# This builds on top of the main OBA Docker image
3+
4+
# First, build the base OBA image if not already built:
5+
# docker build -t oba-base:latest -f ../oba/Dockerfile ../oba
6+
7+
FROM oba-base:latest
8+
9+
# Copy your local GTFS file into the container
10+
# We copy to /tmp first because /bundle gets mounted over
11+
COPY *.zip /tmp/
12+
13+
# Set the GTFS_ZIP_FILENAME environment variable
14+
# This tells the build_bundle.sh script to use the local file instead of downloading
15+
ENV GTFS_ZIP_FILENAME=gtfs.zip
16+
17+
# Copy the entrypoint script
18+
COPY docker-entrypoint.sh /docker-entrypoint.sh
19+
RUN chmod +x /docker-entrypoint.sh
20+
21+
ENTRYPOINT ["/docker-entrypoint.sh"]
22+
23+
# Optional: Override other environment variables if needed
24+
# ENV TZ=America/Los_Angeles
25+
ENV GTFS_TIDY_ARGS=OscRCSmeD
26+
27+
# The entrypoint and other configurations are inherited from the base image

0 commit comments

Comments
 (0)