42Cummer commited on
Commit
d73b136
·
verified ·
1 Parent(s): 5febae6

Upload 3 files

Browse files
Files changed (3) hide show
  1. src/app.py +49 -26
  2. src/db_manager.py +22 -16
  3. 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 (regardless of whether there are active vehicles)
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
- # We use CAST(? AS VARCHAR) to force DuckDB to match strings to strings
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
- # Check if we got ANYTHING back from the DB
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
- name_map = {r[0]: r[3] for r in db_rows}
 
 
 
 
 
 
 
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": name_map.get(str(bus['trip_id']), "Not in Schedule"), # This is the destination
187
  "location": {"lat": bus['lat'], "lon": bus['lon']},
188
- "delay_mins": raw_delay_mins, # Actual integer: 5 = 5m late, -2 = 2m early
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
- # 2. Check if the database is already populated
24
- tables = con.execute("SHOW TABLES").fetchall()
25
- if ('stop_times',) in tables:
26
- print("--- Database already exists and is populated ---")
 
 
27
  return con
28
 
29
- print("--- Initializing DuckDB: Importing CSVs from /static ---")
30
 
31
- # Core GTFS files we need for the rework
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"Loading {f} into table '{table_name}'...")
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
- con.execute(f"CREATE TABLE {table_name} AS SELECT * FROM read_csv_auto('{abs_file_path}')")
 
 
44
  else:
45
- print(f"Error: {file_path} not found! Please ensure it is in the static/ folder.")
 
 
 
 
 
 
 
 
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:5eec5ac01f4d0f0dcd888097054dc7d4e4b849ecbe68012c0c8eb54a4e941d8f
3
- size 53751808
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:77d950fba457220526e7c2bc2bea0a8d12fac55674ee1b576236d2a7dcd332b8
3
+ size 122957824