aboutsummaryrefslogtreecommitdiff
path: root/src/gtfs_vigo_stops
diff options
context:
space:
mode:
authorCopilot <198982749+Copilot@users.noreply.github.com>2025-11-22 18:02:36 +0100
committerGitHub <noreply@github.com>2025-11-22 18:02:36 +0100
commitb96273b54a9b47c79e0afe40a918f751e82097ae (patch)
treeae2990aac150d880df0307124807560cc4593038 /src/gtfs_vigo_stops
parent738cd874fa68cde13dbe6c3f12738abec8e3a8d2 (diff)
Link previous trip shapes for GPS positioning on circular routes (#111)
Co-authored-by: Ariel Costas Guerrero <ariel@costas.dev> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com>
Diffstat (limited to 'src/gtfs_vigo_stops')
-rw-r--r--src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.py43
-rw-r--r--src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.pyi65
-rw-r--r--src/gtfs_vigo_stops/src/report_writer.py1
-rw-r--r--src/gtfs_vigo_stops/src/trips.py20
-rw-r--r--src/gtfs_vigo_stops/stop_report.py138
5 files changed, 202 insertions, 65 deletions
diff --git a/src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.py b/src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.py
index d9f8e52..cb4f336 100644
--- a/src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.py
+++ b/src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.py
@@ -1,22 +1,11 @@
# -*- coding: utf-8 -*-
# Generated by the protocol buffer compiler. DO NOT EDIT!
-# NO CHECKED-IN PROTOBUF GENCODE
# source: stop_schedule.proto
-# Protobuf Python Version: 6.33.0
"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
from google.protobuf import descriptor as _descriptor
from google.protobuf import descriptor_pool as _descriptor_pool
-from google.protobuf import runtime_version as _runtime_version
from google.protobuf import symbol_database as _symbol_database
-from google.protobuf.internal import builder as _builder
-_runtime_version.ValidateProtobufRuntimeVersion(
- _runtime_version.Domain.PUBLIC,
- 6,
- 33,
- 0,
- '',
- 'stop_schedule.proto'
-)
# @@protoc_insertion_point(imports)
_sym_db = _symbol_database.Default()
@@ -24,20 +13,20 @@ _sym_db = _symbol_database.Default()
-DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13stop_schedule.proto\x12\x05proto\"!\n\tEpsg25829\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\"\xe3\x03\n\x0cStopArrivals\x12\x0f\n\x07stop_id\x18\x01 \x01(\t\x12\"\n\x08location\x18\x03 \x01(\x0b\x32\x10.proto.Epsg25829\x12\x36\n\x08\x61rrivals\x18\x05 \x03(\x0b\x32$.proto.StopArrivals.ScheduledArrival\x1a\xe5\x02\n\x10ScheduledArrival\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x0f\n\x07trip_id\x18\x02 \x01(\t\x12\x0c\n\x04line\x18\x03 \x01(\t\x12\r\n\x05route\x18\x04 \x01(\t\x12\x10\n\x08shape_id\x18\x05 \x01(\t\x12\x1b\n\x13shape_dist_traveled\x18\x06 \x01(\x01\x12\x15\n\rstop_sequence\x18\x0b \x01(\r\x12\x14\n\x0cnext_streets\x18\x0c \x03(\t\x12\x15\n\rstarting_code\x18\x15 \x01(\t\x12\x15\n\rstarting_name\x18\x16 \x01(\t\x12\x15\n\rstarting_time\x18\x17 \x01(\t\x12\x14\n\x0c\x63\x61lling_time\x18! \x01(\t\x12\x13\n\x0b\x63\x61lling_ssm\x18\" \x01(\r\x12\x15\n\rterminus_code\x18) \x01(\t\x12\x15\n\rterminus_name\x18* \x01(\t\x12\x15\n\rterminus_time\x18+ \x01(\t\";\n\x05Shape\x12\x10\n\x08shape_id\x18\x01 \x01(\t\x12 \n\x06points\x18\x03 \x03(\x0b\x32\x10.proto.Epsg25829B$\xaa\x02!Costasdev.Busurbano.Backend.Typesb\x06proto3')
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x13stop_schedule.proto\x12\x05proto\"!\n\tEpsg25829\x12\t\n\x01x\x18\x01 \x01(\x01\x12\t\n\x01y\x18\x02 \x01(\x01\"\x83\x04\n\x0cStopArrivals\x12\x0f\n\x07stop_id\x18\x01 \x01(\t\x12\"\n\x08location\x18\x03 \x01(\x0b\x32\x10.proto.Epsg25829\x12\x36\n\x08\x61rrivals\x18\x05 \x03(\x0b\x32$.proto.StopArrivals.ScheduledArrival\x1a\x85\x03\n\x10ScheduledArrival\x12\x12\n\nservice_id\x18\x01 \x01(\t\x12\x0f\n\x07trip_id\x18\x02 \x01(\t\x12\x0c\n\x04line\x18\x03 \x01(\t\x12\r\n\x05route\x18\x04 \x01(\t\x12\x10\n\x08shape_id\x18\x05 \x01(\t\x12\x1b\n\x13shape_dist_traveled\x18\x06 \x01(\x01\x12\x15\n\rstop_sequence\x18\x0b \x01(\r\x12\x14\n\x0cnext_streets\x18\x0c \x03(\t\x12\x15\n\rstarting_code\x18\x15 \x01(\t\x12\x15\n\rstarting_name\x18\x16 \x01(\t\x12\x15\n\rstarting_time\x18\x17 \x01(\t\x12\x14\n\x0c\x63\x61lling_time\x18! \x01(\t\x12\x13\n\x0b\x63\x61lling_ssm\x18\" \x01(\r\x12\x15\n\rterminus_code\x18) \x01(\t\x12\x15\n\rterminus_name\x18* \x01(\t\x12\x15\n\rterminus_time\x18+ \x01(\t\x12\x1e\n\x16previous_trip_shape_id\x18\x33 \x01(\t\";\n\x05Shape\x12\x10\n\x08shape_id\x18\x01 \x01(\t\x12 \n\x06points\x18\x03 \x03(\x0b\x32\x10.proto.Epsg25829B$\xaa\x02!Costasdev.Busurbano.Backend.Typesb\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'stop_schedule_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
-_globals = globals()
-_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
-_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'stop_schedule_pb2', _globals)
-if not _descriptor._USE_C_DESCRIPTORS:
- _globals['DESCRIPTOR']._loaded_options = None
- _globals['DESCRIPTOR']._serialized_options = b'\252\002!Costasdev.Busurbano.Backend.Types'
- _globals['_EPSG25829']._serialized_start=30
- _globals['_EPSG25829']._serialized_end=63
- _globals['_STOPARRIVALS']._serialized_start=66
- _globals['_STOPARRIVALS']._serialized_end=549
- _globals['_STOPARRIVALS_SCHEDULEDARRIVAL']._serialized_start=192
- _globals['_STOPARRIVALS_SCHEDULEDARRIVAL']._serialized_end=549
- _globals['_SHAPE']._serialized_start=551
- _globals['_SHAPE']._serialized_end=610
+ DESCRIPTOR._options = None
+ DESCRIPTOR._serialized_options = b'\252\002!Costasdev.Busurbano.Backend.Types'
+ _EPSG25829._serialized_start=30
+ _EPSG25829._serialized_end=63
+ _STOPARRIVALS._serialized_start=66
+ _STOPARRIVALS._serialized_end=581
+ _STOPARRIVALS_SCHEDULEDARRIVAL._serialized_start=192
+ _STOPARRIVALS_SCHEDULEDARRIVAL._serialized_end=581
+ _SHAPE._serialized_start=583
+ _SHAPE._serialized_end=642
# @@protoc_insertion_point(module_scope)
diff --git a/src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.pyi b/src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.pyi
index 615999f..355798f 100644
--- a/src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.pyi
+++ b/src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.pyi
@@ -1,68 +1,69 @@
from google.protobuf.internal import containers as _containers
from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
-from collections.abc import Iterable as _Iterable, Mapping as _Mapping
-from typing import ClassVar as _ClassVar, Optional as _Optional, Union as _Union
+from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
DESCRIPTOR: _descriptor.FileDescriptor
class Epsg25829(_message.Message):
- __slots__ = ()
+ __slots__ = ["x", "y"]
X_FIELD_NUMBER: _ClassVar[int]
Y_FIELD_NUMBER: _ClassVar[int]
x: float
y: float
def __init__(self, x: _Optional[float] = ..., y: _Optional[float] = ...) -> None: ...
+class Shape(_message.Message):
+ __slots__ = ["points", "shape_id"]
+ POINTS_FIELD_NUMBER: _ClassVar[int]
+ SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
+ points: _containers.RepeatedCompositeFieldContainer[Epsg25829]
+ shape_id: str
+ def __init__(self, shape_id: _Optional[str] = ..., points: _Optional[_Iterable[_Union[Epsg25829, _Mapping]]] = ...) -> None: ...
+
class StopArrivals(_message.Message):
- __slots__ = ()
+ __slots__ = ["arrivals", "location", "stop_id"]
class ScheduledArrival(_message.Message):
- __slots__ = ()
- SERVICE_ID_FIELD_NUMBER: _ClassVar[int]
- TRIP_ID_FIELD_NUMBER: _ClassVar[int]
+ __slots__ = ["calling_ssm", "calling_time", "line", "next_streets", "previous_trip_shape_id", "route", "service_id", "shape_dist_traveled", "shape_id", "starting_code", "starting_name", "starting_time", "stop_sequence", "terminus_code", "terminus_name", "terminus_time", "trip_id"]
+ CALLING_SSM_FIELD_NUMBER: _ClassVar[int]
+ CALLING_TIME_FIELD_NUMBER: _ClassVar[int]
LINE_FIELD_NUMBER: _ClassVar[int]
+ NEXT_STREETS_FIELD_NUMBER: _ClassVar[int]
+ PREVIOUS_TRIP_SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
ROUTE_FIELD_NUMBER: _ClassVar[int]
- SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
+ SERVICE_ID_FIELD_NUMBER: _ClassVar[int]
SHAPE_DIST_TRAVELED_FIELD_NUMBER: _ClassVar[int]
- STOP_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
- NEXT_STREETS_FIELD_NUMBER: _ClassVar[int]
+ SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
STARTING_CODE_FIELD_NUMBER: _ClassVar[int]
STARTING_NAME_FIELD_NUMBER: _ClassVar[int]
STARTING_TIME_FIELD_NUMBER: _ClassVar[int]
- CALLING_TIME_FIELD_NUMBER: _ClassVar[int]
- CALLING_SSM_FIELD_NUMBER: _ClassVar[int]
+ STOP_SEQUENCE_FIELD_NUMBER: _ClassVar[int]
TERMINUS_CODE_FIELD_NUMBER: _ClassVar[int]
TERMINUS_NAME_FIELD_NUMBER: _ClassVar[int]
TERMINUS_TIME_FIELD_NUMBER: _ClassVar[int]
- service_id: str
- trip_id: str
+ TRIP_ID_FIELD_NUMBER: _ClassVar[int]
+ calling_ssm: int
+ calling_time: str
line: str
+ next_streets: _containers.RepeatedScalarFieldContainer[str]
+ previous_trip_shape_id: str
route: str
- shape_id: str
+ service_id: str
shape_dist_traveled: float
- stop_sequence: int
- next_streets: _containers.RepeatedScalarFieldContainer[str]
+ shape_id: str
starting_code: str
starting_name: str
starting_time: str
- calling_time: str
- calling_ssm: int
+ stop_sequence: int
terminus_code: str
terminus_name: str
terminus_time: str
- def __init__(self, service_id: _Optional[str] = ..., trip_id: _Optional[str] = ..., line: _Optional[str] = ..., route: _Optional[str] = ..., shape_id: _Optional[str] = ..., shape_dist_traveled: _Optional[float] = ..., stop_sequence: _Optional[int] = ..., next_streets: _Optional[_Iterable[str]] = ..., starting_code: _Optional[str] = ..., starting_name: _Optional[str] = ..., starting_time: _Optional[str] = ..., calling_time: _Optional[str] = ..., calling_ssm: _Optional[int] = ..., terminus_code: _Optional[str] = ..., terminus_name: _Optional[str] = ..., terminus_time: _Optional[str] = ...) -> None: ...
- STOP_ID_FIELD_NUMBER: _ClassVar[int]
- LOCATION_FIELD_NUMBER: _ClassVar[int]
+ trip_id: str
+ def __init__(self, service_id: _Optional[str] = ..., trip_id: _Optional[str] = ..., line: _Optional[str] = ..., route: _Optional[str] = ..., shape_id: _Optional[str] = ..., shape_dist_traveled: _Optional[float] = ..., stop_sequence: _Optional[int] = ..., next_streets: _Optional[_Iterable[str]] = ..., starting_code: _Optional[str] = ..., starting_name: _Optional[str] = ..., starting_time: _Optional[str] = ..., calling_time: _Optional[str] = ..., calling_ssm: _Optional[int] = ..., terminus_code: _Optional[str] = ..., terminus_name: _Optional[str] = ..., terminus_time: _Optional[str] = ..., previous_trip_shape_id: _Optional[str] = ...) -> None: ...
ARRIVALS_FIELD_NUMBER: _ClassVar[int]
- stop_id: str
- location: Epsg25829
+ LOCATION_FIELD_NUMBER: _ClassVar[int]
+ STOP_ID_FIELD_NUMBER: _ClassVar[int]
arrivals: _containers.RepeatedCompositeFieldContainer[StopArrivals.ScheduledArrival]
+ location: Epsg25829
+ stop_id: str
def __init__(self, stop_id: _Optional[str] = ..., location: _Optional[_Union[Epsg25829, _Mapping]] = ..., arrivals: _Optional[_Iterable[_Union[StopArrivals.ScheduledArrival, _Mapping]]] = ...) -> None: ...
-
-class Shape(_message.Message):
- __slots__ = ()
- SHAPE_ID_FIELD_NUMBER: _ClassVar[int]
- POINTS_FIELD_NUMBER: _ClassVar[int]
- shape_id: str
- points: _containers.RepeatedCompositeFieldContainer[Epsg25829]
- def __init__(self, shape_id: _Optional[str] = ..., points: _Optional[_Iterable[_Union[Epsg25829, _Mapping]]] = ...) -> None: ...
diff --git a/src/gtfs_vigo_stops/src/report_writer.py b/src/gtfs_vigo_stops/src/report_writer.py
index 695931f..f6d8763 100644
--- a/src/gtfs_vigo_stops/src/report_writer.py
+++ b/src/gtfs_vigo_stops/src/report_writer.py
@@ -51,6 +51,7 @@ def write_stop_protobuf(
terminus_code=arrival["terminus_code"],
terminus_name=arrival["terminus_name"],
terminus_time=arrival["terminus_time"],
+ previous_trip_shape_id=arrival.get("previous_trip_shape_id", ""),
)
for arrival in arrivals
],
diff --git a/src/gtfs_vigo_stops/src/trips.py b/src/gtfs_vigo_stops/src/trips.py
index 0c1375c..0cedd26 100644
--- a/src/gtfs_vigo_stops/src/trips.py
+++ b/src/gtfs_vigo_stops/src/trips.py
@@ -10,18 +10,19 @@ class TripLine:
"""
Class representing a trip line in the GTFS data.
"""
- def __init__(self, route_id: str, service_id: str, trip_id: str, headsign: str, direction_id: int, shape_id: str|None = None):
+ def __init__(self, route_id: str, service_id: str, trip_id: str, headsign: str, direction_id: int, shape_id: str|None = None, block_id: str|None = None):
self.route_id = route_id
self.service_id = service_id
self.trip_id = trip_id
self.headsign = headsign
self.direction_id = direction_id
self.shape_id = shape_id
+ self.block_id = block_id
self.route_short_name = ""
self.route_color = ""
def __str__(self):
- return f"TripLine({self.route_id=}, {self.service_id=}, {self.trip_id=}, {self.headsign=}, {self.direction_id=}, {self.shape_id=})"
+ return f"TripLine({self.route_id=}, {self.service_id=}, {self.trip_id=}, {self.headsign=}, {self.direction_id=}, {self.shape_id=}, {self.block_id=})"
TRIPS_BY_SERVICE_ID: dict[str, dict[str, list[TripLine]]] = {}
@@ -74,6 +75,13 @@ def get_trips_for_services(feed_dir: str, service_ids: list[str]) -> dict[str, l
else:
logger.warning("shape_id column not found in trips.txt")
+ # Check if block_id column exists
+ block_id_index = None
+ if 'block_id' in header:
+ block_id_index = header.index('block_id')
+ else:
+ logger.info("block_id column not found in trips.txt")
+
# Initialize cache for this feed directory
TRIPS_BY_SERVICE_ID[feed_dir] = {}
@@ -96,6 +104,11 @@ def get_trips_for_services(feed_dir: str, service_ids: list[str]) -> dict[str, l
if shape_id_index is not None and shape_id_index < len(parts):
shape_id = parts[shape_id_index] if parts[shape_id_index] else None
+ # Get block_id if available
+ block_id = None
+ if block_id_index is not None and block_id_index < len(parts):
+ block_id = parts[block_id_index] if parts[block_id_index] else None
+
trip_line = TripLine(
route_id=parts[route_id_index],
service_id=service_id,
@@ -103,7 +116,8 @@ def get_trips_for_services(feed_dir: str, service_ids: list[str]) -> dict[str, l
headsign=parts[headsign_index],
direction_id=int(
parts[direction_id_index] if parts[direction_id_index] else -1),
- shape_id=shape_id
+ shape_id=shape_id,
+ block_id=block_id
)
TRIPS_BY_SERVICE_ID[feed_dir][service_id].append(trip_line)
diff --git a/src/gtfs_vigo_stops/stop_report.py b/src/gtfs_vigo_stops/stop_report.py
index a827eaa..cee11ea 100644
--- a/src/gtfs_vigo_stops/stop_report.py
+++ b/src/gtfs_vigo_stops/stop_report.py
@@ -4,7 +4,7 @@ import shutil
import sys
import time
import traceback
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional, Tuple
from src.shapes import process_shapes
from src.common import get_all_feed_dates
@@ -13,10 +13,10 @@ from src.logger import get_logger
from src.report_writer import write_stop_json, write_stop_protobuf
from src.routes import load_routes
from src.services import get_active_services
-from src.stop_times import get_stops_for_trips
+from src.stop_times import get_stops_for_trips, StopTime
from src.stops import get_all_stops, get_all_stops_by_code, get_numeric_code
from src.street_name import get_street_name, normalise_stop_name
-from src.trips import get_trips_for_services
+from src.trips import get_trips_for_services, TripLine
logger = get_logger("stop_report")
@@ -143,6 +143,130 @@ def is_next_day_service(time_str: str) -> bool:
return False
+def parse_trip_id_components(trip_id: str) -> Optional[Tuple[str, str, int]]:
+ """
+ Parse a trip ID in format XXXYYY-Z or XXXYYY_Z where:
+ - XXX = line number (e.g., 003)
+ - YYY = shift/internal ID (e.g., 001)
+ - Z = trip number (e.g., 12)
+
+ Supported formats:
+ 1. ..._XXXYYY_Z (e.g. "C1 01SNA00_001001_18")
+ 2. ..._XXXYYY-Z (e.g. "VIGO_20241122_003001-12")
+
+ Returns tuple of (line, shift_id, trip_number) or None if parsing fails.
+ """
+ try:
+ parts = trip_id.split("_")
+ if len(parts) < 2:
+ return None
+
+ # Try format 1: ..._XXXYYY_Z
+ # Check if second to last part is 6 digits (XXXYYY) and last part is numeric
+ if len(parts) >= 2:
+ shift_part = parts[-2]
+ trip_num_str = parts[-1]
+
+ if len(shift_part) == 6 and shift_part.isdigit() and trip_num_str.isdigit():
+ line = shift_part[:3]
+ shift_id = shift_part[3:6]
+ trip_number = int(trip_num_str)
+ return (line, shift_id, trip_number)
+
+ # Try format 2: ..._XXXYYY-Z
+ # The trip ID is the last part in format XXXYYY-Z
+ trip_part = parts[-1]
+
+ if "-" in trip_part:
+ shift_part, trip_num_str = trip_part.split("-", 1)
+
+ # shift_part should be 6 digits: XXXYYY
+ if len(shift_part) == 6 and shift_part.isdigit():
+ line = shift_part[:3] # First 3 digits
+ shift_id = shift_part[3:6] # Next 3 digits
+ trip_number = int(trip_num_str)
+ return (line, shift_id, trip_number)
+
+ return None
+ except (ValueError, IndexError):
+ return None
+
+
+def build_trip_previous_shape_map(
+ trips: Dict[str, List[TripLine]],
+ stops_for_all_trips: Dict[str, List[StopTime]],
+) -> Dict[str, Optional[str]]:
+ """
+ Build a mapping from trip_id to previous_trip_shape_id.
+
+ Links trips based on trip ID structure (XXXYYY-Z) where trips with the same
+ XXX (line) and YYY (shift) and sequential Z (trip numbers) are connected
+ if the terminus of trip N matches the start of trip N+1.
+
+ Args:
+ trips: Dictionary of service_id -> list of trips
+ stops_for_all_trips: Dictionary of trip_id -> list of stop times
+
+ Returns:
+ Dictionary mapping trip_id to previous_trip_shape_id (or None)
+ """
+ trip_previous_shape: Dict[str, Optional[str]] = {}
+
+ # Collect all trips across all services
+ all_trips_list: List[TripLine] = []
+ for trip_list in trips.values():
+ all_trips_list.extend(trip_list)
+
+ # Group trips by shift ID (line + shift combination)
+ trips_by_shift: Dict[str, List[Tuple[TripLine, int, str, str]]] = {}
+
+ for trip in all_trips_list:
+ parsed = parse_trip_id_components(trip.trip_id)
+ if not parsed:
+ continue
+
+ line, shift_id, trip_number = parsed
+ shift_key = f"{line}{shift_id}"
+
+ trip_stops = stops_for_all_trips.get(trip.trip_id)
+ if not trip_stops or len(trip_stops) < 2:
+ continue
+
+ first_stop = trip_stops[0]
+ last_stop = trip_stops[-1]
+
+ if shift_key not in trips_by_shift:
+ trips_by_shift[shift_key] = []
+
+ trips_by_shift[shift_key].append((
+ trip,
+ trip_number,
+ first_stop.stop_id,
+ last_stop.stop_id
+ ))
+
+ # For each shift, sort trips by trip number and link consecutive trips
+ for shift_key, shift_trips in trips_by_shift.items():
+ # Sort by trip number
+ shift_trips.sort(key=lambda x: x[1])
+
+ # Link consecutive trips if their stops match
+ for i in range(1, len(shift_trips)):
+ current_trip, current_num, current_start_stop, _ = shift_trips[i]
+ prev_trip, prev_num, _, prev_end_stop = shift_trips[i - 1]
+
+ # Check if trips are consecutive (trip numbers differ by 1),
+ # if previous trip's terminus matches current trip's start,
+ # and if both trips have valid shape IDs
+ if (current_num == prev_num + 1 and
+ prev_end_stop == current_start_stop and
+ prev_trip.shape_id and
+ current_trip.shape_id):
+ trip_previous_shape[current_trip.trip_id] = prev_trip.shape_id
+
+ return trip_previous_shape
+
+
def get_stop_arrivals(feed_dir: str, date: str) -> Dict[str, List[Dict[str, Any]]]:
"""
Process trips for the given date and organize stop arrivals.
@@ -192,6 +316,10 @@ def get_stop_arrivals(feed_dir: str, date: str) -> Dict[str, List[Dict[str, Any]
stops_for_all_trips = get_stops_for_trips(feed_dir, all_trip_ids)
logger.info(f"Precomputed stops for {len(stops_for_all_trips)} trips.")
+ # Build mapping from trip_id to previous trip's shape_id
+ trip_previous_shape_map = build_trip_previous_shape_map(trips, stops_for_all_trips)
+ logger.info(f"Built previous trip shape mapping for {len(trip_previous_shape_map)} trips.")
+
# Load routes information
routes = load_routes(feed_dir)
logger.info(f"Loaded {len(routes)} routes from feed.")
@@ -335,6 +463,9 @@ def get_stop_arrivals(feed_dir: str, date: str) -> Dict[str, List[Dict[str, Any]
next_streets = []
trip_id_fmt = "_".join(trip_id.split("_")[1:3])
+
+ # Get previous trip shape_id if available
+ previous_trip_shape_id = trip_previous_shape_map.get(trip_id, "")
stop_arrivals[stop_code].append(
{
@@ -356,6 +487,7 @@ def get_stop_arrivals(feed_dir: str, date: str) -> Dict[str, List[Dict[str, Any]
"terminus_code": terminus_code,
"terminus_name": terminus_name,
"terminus_time": final_terminus_time,
+ "previous_trip_shape_id": previous_trip_shape_id,
}
)