Spaces:
Running
Running
Upload 3 files
Browse files- src/app.py +49 -26
- src/db_manager.py +22 -16
- src/ttc_gtfs.duckdb +2 -2
src/app.py
CHANGED
|
@@ -99,7 +99,7 @@ async def get_route_view(route_id: str):
|
|
| 99 |
all_buses = data.get("vehicles", [])
|
| 100 |
route_buses = [v for v in all_buses if v['route'] == route_id]
|
| 101 |
|
| 102 |
-
# Get all stops for this route
|
| 103 |
stops_query = """
|
| 104 |
SELECT DISTINCT
|
| 105 |
s.stop_id,
|
|
@@ -121,72 +121,69 @@ async def get_route_view(route_id: str):
|
|
| 121 |
"stop_id": str(r[0]),
|
| 122 |
"stop_code": str(r[1]) if r[1] else None,
|
| 123 |
"stop_name": r[2],
|
| 124 |
-
"location": {
|
| 125 |
-
"lat": r[3],
|
| 126 |
-
"lon": r[4]
|
| 127 |
-
}
|
| 128 |
}
|
| 129 |
for r in stops_results
|
| 130 |
]
|
| 131 |
|
| 132 |
-
# If no active vehicles, return just the stops
|
| 133 |
if not route_buses:
|
| 134 |
-
return {
|
| 135 |
-
"route": route_id,
|
| 136 |
-
"vehicles": [],
|
| 137 |
-
"stops": stops
|
| 138 |
-
}
|
| 139 |
|
| 140 |
-
# IMPORTANT: Cast Trip IDs to strings to ensure they match the DB
|
| 141 |
trip_ids = [str(v['trip_id']) for v in route_buses]
|
| 142 |
placeholders = ','.join(['?'] * len(trip_ids))
|
| 143 |
|
| 144 |
-
#
|
| 145 |
query = f"""
|
| 146 |
SELECT
|
| 147 |
CAST(st.trip_id AS VARCHAR),
|
| 148 |
CAST(st.stop_id AS VARCHAR),
|
| 149 |
st.arrival_time as scheduled_time,
|
| 150 |
-
t.trip_headsign
|
|
|
|
| 151 |
FROM stop_times st
|
| 152 |
JOIN trips t ON CAST(st.trip_id AS VARCHAR) = CAST(t.trip_id AS VARCHAR)
|
| 153 |
WHERE CAST(st.trip_id AS VARCHAR) IN ({placeholders})
|
| 154 |
"""
|
| 155 |
-
db_rows = db.execute(query, trip_ids).fetchall()
|
| 156 |
|
| 157 |
-
|
| 158 |
-
if not db_rows:
|
| 159 |
-
print(f"DEBUG: No matches in DB for Trip IDs: {trip_ids[:3]}")
|
| 160 |
|
|
|
|
| 161 |
schedule_map = {(r[0], r[1]): r[2] for r in db_rows}
|
| 162 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
|
| 164 |
service_day_ts = get_service_day_start_ts()
|
| 165 |
enriched = []
|
| 166 |
|
| 167 |
for bus in route_buses:
|
| 168 |
-
# Default delay is 0 if no prediction exists
|
| 169 |
raw_delay_mins = 0
|
| 170 |
-
|
| 171 |
pred_time = bus.get('predicted_time')
|
| 172 |
stop_id = bus.get('next_stop_id')
|
| 173 |
|
| 174 |
if pred_time and stop_id:
|
| 175 |
sched_hms = schedule_map.get((str(bus['trip_id']), str(stop_id)))
|
| 176 |
if sched_hms:
|
| 177 |
-
# DELAY = SCHEDULED - PREDICTED (negative = late, positive = early)
|
| 178 |
-
# Handle GTFS times >= 24 hours (next day)
|
| 179 |
h, m, s = map(int, sched_hms.split(':'))
|
| 180 |
extra_days = h // 24
|
| 181 |
plan_ts = service_day_ts + (extra_days * 86400) + hms_to_seconds(sched_hms)
|
| 182 |
raw_delay_mins = round((plan_ts - pred_time) / 60)
|
| 183 |
|
|
|
|
|
|
|
|
|
|
| 184 |
enriched.append({
|
| 185 |
"number": bus['id'],
|
| 186 |
-
"name":
|
| 187 |
"location": {"lat": bus['lat'], "lon": bus['lon']},
|
| 188 |
-
"delay_mins": raw_delay_mins,
|
| 189 |
-
"fullness": translate_occupancy(bus['occupancy'])
|
|
|
|
| 190 |
})
|
| 191 |
|
| 192 |
return {
|
|
@@ -521,6 +518,32 @@ async def get_nearby_context(lat: float, lon: float):
|
|
| 521 |
]
|
| 522 |
}
|
| 523 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
if __name__ == "__main__":
|
| 525 |
import uvicorn # type: ignore
|
| 526 |
# Start the server
|
|
|
|
| 99 |
all_buses = data.get("vehicles", [])
|
| 100 |
route_buses = [v for v in all_buses if v['route'] == route_id]
|
| 101 |
|
| 102 |
+
# Get all stops for this route
|
| 103 |
stops_query = """
|
| 104 |
SELECT DISTINCT
|
| 105 |
s.stop_id,
|
|
|
|
| 121 |
"stop_id": str(r[0]),
|
| 122 |
"stop_code": str(r[1]) if r[1] else None,
|
| 123 |
"stop_name": r[2],
|
| 124 |
+
"location": {"lat": r[3], "lon": r[4]}
|
|
|
|
|
|
|
|
|
|
| 125 |
}
|
| 126 |
for r in stops_results
|
| 127 |
]
|
| 128 |
|
|
|
|
| 129 |
if not route_buses:
|
| 130 |
+
return {"route": route_id, "vehicles": [], "stops": stops}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
|
|
|
| 132 |
trip_ids = [str(v['trip_id']) for v in route_buses]
|
| 133 |
placeholders = ','.join(['?'] * len(trip_ids))
|
| 134 |
|
| 135 |
+
# Updated query to include shape_id
|
| 136 |
query = f"""
|
| 137 |
SELECT
|
| 138 |
CAST(st.trip_id AS VARCHAR),
|
| 139 |
CAST(st.stop_id AS VARCHAR),
|
| 140 |
st.arrival_time as scheduled_time,
|
| 141 |
+
t.trip_headsign,
|
| 142 |
+
t.shape_id
|
| 143 |
FROM stop_times st
|
| 144 |
JOIN trips t ON CAST(st.trip_id AS VARCHAR) = CAST(t.trip_id AS VARCHAR)
|
| 145 |
WHERE CAST(st.trip_id AS VARCHAR) IN ({placeholders})
|
| 146 |
"""
|
|
|
|
| 147 |
|
| 148 |
+
db_rows = db.execute(query, trip_ids).fetchall()
|
|
|
|
|
|
|
| 149 |
|
| 150 |
+
# Map (trip_id, stop_id) to scheduled time
|
| 151 |
schedule_map = {(r[0], r[1]): r[2] for r in db_rows}
|
| 152 |
+
|
| 153 |
+
# Map trip_id to an object containing headsign and shape_id
|
| 154 |
+
name_map = {
|
| 155 |
+
r[0]: {
|
| 156 |
+
"headsign": r[3],
|
| 157 |
+
"shape_id": r[4]
|
| 158 |
+
} for r in db_rows
|
| 159 |
+
}
|
| 160 |
|
| 161 |
service_day_ts = get_service_day_start_ts()
|
| 162 |
enriched = []
|
| 163 |
|
| 164 |
for bus in route_buses:
|
|
|
|
| 165 |
raw_delay_mins = 0
|
|
|
|
| 166 |
pred_time = bus.get('predicted_time')
|
| 167 |
stop_id = bus.get('next_stop_id')
|
| 168 |
|
| 169 |
if pred_time and stop_id:
|
| 170 |
sched_hms = schedule_map.get((str(bus['trip_id']), str(stop_id)))
|
| 171 |
if sched_hms:
|
|
|
|
|
|
|
| 172 |
h, m, s = map(int, sched_hms.split(':'))
|
| 173 |
extra_days = h // 24
|
| 174 |
plan_ts = service_day_ts + (extra_days * 86400) + hms_to_seconds(sched_hms)
|
| 175 |
raw_delay_mins = round((plan_ts - pred_time) / 60)
|
| 176 |
|
| 177 |
+
# Pull details from our updated name_map
|
| 178 |
+
trip_info = name_map.get(str(bus['trip_id']), {})
|
| 179 |
+
|
| 180 |
enriched.append({
|
| 181 |
"number": bus['id'],
|
| 182 |
+
"name": trip_info.get("headsign", "Not in Schedule"),
|
| 183 |
"location": {"lat": bus['lat'], "lon": bus['lon']},
|
| 184 |
+
"delay_mins": raw_delay_mins,
|
| 185 |
+
"fullness": translate_occupancy(bus['occupancy']),
|
| 186 |
+
"shape_id": trip_info.get("shape_id") # Bonus feature ready!
|
| 187 |
})
|
| 188 |
|
| 189 |
return {
|
|
|
|
| 518 |
]
|
| 519 |
}
|
| 520 |
|
| 521 |
+
@app.get("/api/shapes/{shape_id}")
|
| 522 |
+
async def get_route_shape(shape_id: str):
|
| 523 |
+
"""
|
| 524 |
+
Returns the ordered list of lat/lon coordinates for a specific route shape.
|
| 525 |
+
"""
|
| 526 |
+
try:
|
| 527 |
+
# Query ordered by sequence to ensure the line draws correctly
|
| 528 |
+
query = """
|
| 529 |
+
SELECT shape_pt_lat, shape_pt_lon
|
| 530 |
+
FROM shapes
|
| 531 |
+
WHERE CAST(shape_id AS VARCHAR) = ?
|
| 532 |
+
ORDER BY shape_pt_sequence ASC
|
| 533 |
+
"""
|
| 534 |
+
results = db.execute(query, [shape_id]).fetchall()
|
| 535 |
+
|
| 536 |
+
# Format for Leaflet: [[lat, lon], [lat, lon], ...]
|
| 537 |
+
path = [[r[0], r[1]] for r in results]
|
| 538 |
+
|
| 539 |
+
return {
|
| 540 |
+
"status": "success",
|
| 541 |
+
"shape_id": shape_id,
|
| 542 |
+
"coordinates": path
|
| 543 |
+
}
|
| 544 |
+
except Exception as e:
|
| 545 |
+
return {"status": "error", "message": str(e)}
|
| 546 |
+
|
| 547 |
if __name__ == "__main__":
|
| 548 |
import uvicorn # type: ignore
|
| 549 |
# Start the server
|
src/db_manager.py
CHANGED
|
@@ -14,35 +14,41 @@ DB_PATH = str(Path(__file__).parent / "ttc_gtfs.duckdb")
|
|
| 14 |
STATIC_DIR = str(Path(__file__).parent.parent / "static")
|
| 15 |
|
| 16 |
def init_db():
|
| 17 |
-
"""
|
| 18 |
-
Connects to DuckDB and imports the GTFS-Static data from the static/ directory.
|
| 19 |
-
"""
|
| 20 |
-
# 1. Connect to DuckDB (creates the file if it doesn't exist)
|
| 21 |
con = duckdb.connect(DB_PATH)
|
| 22 |
|
| 23 |
-
#
|
| 24 |
-
tables = con.execute("SHOW TABLES").fetchall()
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
| 27 |
return con
|
| 28 |
|
| 29 |
-
print("--- Initializing DuckDB: Importing CSVs
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
files = ["routes.txt", "trips.txt", "stops.txt", "stop_times.txt"]
|
| 33 |
|
| 34 |
for f in files:
|
| 35 |
file_path = Path(STATIC_DIR) / f
|
| 36 |
table_name = f.replace(".txt", "")
|
| 37 |
|
| 38 |
if file_path.exists():
|
| 39 |
-
print(f"
|
| 40 |
-
# 'read_csv_auto' automatically detects headers and data types
|
| 41 |
-
# Use absolute path for DuckDB
|
| 42 |
abs_file_path = str(file_path.resolve())
|
| 43 |
-
|
|
|
|
|
|
|
| 44 |
else:
|
| 45 |
-
print(f"Error: {file_path} not found!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
print("--- Database Import Complete ---")
|
| 48 |
return con
|
|
|
|
| 14 |
STATIC_DIR = str(Path(__file__).parent.parent / "static")
|
| 15 |
|
| 16 |
def init_db():
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
con = duckdb.connect(DB_PATH)
|
| 18 |
|
| 19 |
+
# 1. Check for 'shapes' specifically
|
| 20 |
+
tables = [t[0] for t in con.execute("SHOW TABLES").fetchall()]
|
| 21 |
+
|
| 22 |
+
# If all core tables including shapes exist, skip the import
|
| 23 |
+
if all(t in tables for t in ["routes", "trips", "stops", "stop_times", "shapes"]):
|
| 24 |
+
print("--- Database fully populated (including shapes) ---")
|
| 25 |
return con
|
| 26 |
|
| 27 |
+
print("--- Initializing/Updating DuckDB: Importing CSVs ---")
|
| 28 |
|
| 29 |
+
# Updated files list to include the missing shapes
|
| 30 |
+
files = ["routes.txt", "trips.txt", "stops.txt", "stop_times.txt", "shapes.txt"]
|
| 31 |
|
| 32 |
for f in files:
|
| 33 |
file_path = Path(STATIC_DIR) / f
|
| 34 |
table_name = f.replace(".txt", "")
|
| 35 |
|
| 36 |
if file_path.exists():
|
| 37 |
+
print(f"Importing {f} into table '{table_name}'...")
|
|
|
|
|
|
|
| 38 |
abs_file_path = str(file_path.resolve())
|
| 39 |
+
|
| 40 |
+
# Use 'CREATE OR REPLACE' to overwrite existing tables without crashing
|
| 41 |
+
con.execute(f"CREATE OR REPLACE TABLE {table_name} AS SELECT * FROM read_csv_auto('{abs_file_path}')")
|
| 42 |
else:
|
| 43 |
+
print(f"Error: {file_path} not found!")
|
| 44 |
+
|
| 45 |
+
# Add this inside init_db in db_manager.py after the file import loop
|
| 46 |
+
print("--- Creating Indexes for Performance ---")
|
| 47 |
+
# This speeds up the /api/shapes/{shape_id} endpoint significantly
|
| 48 |
+
con.execute("CREATE INDEX IF NOT EXISTS idx_shape_id ON shapes (shape_id)")
|
| 49 |
+
|
| 50 |
+
# While you're at it, indexing trip_id in stop_times speeds up your arrival logic
|
| 51 |
+
con.execute("CREATE INDEX IF NOT EXISTS idx_stop_times_trip_id ON stop_times (trip_id)")
|
| 52 |
|
| 53 |
print("--- Database Import Complete ---")
|
| 54 |
return con
|
src/ttc_gtfs.duckdb
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
-
size
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:77d950fba457220526e7c2bc2bea0a8d12fac55674ee1b576236d2a7dcd332b8
|
| 3 |
+
size 122957824
|