From b96273b54a9b47c79e0afe40a918f751e82097ae Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Nov 2025 18:02:36 +0100 Subject: Link previous trip shapes for GPS positioning on circular routes (#111) Co-authored-by: Ariel Costas Guerrero Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: arielcostas <94913521+arielcostas@users.noreply.github.com> --- src/gtfs_vigo_stops/src/proto/stop_schedule_pb2.py | 43 +++---- .../src/proto/stop_schedule_pb2.pyi | 65 +++++----- src/gtfs_vigo_stops/src/report_writer.py | 1 + src/gtfs_vigo_stops/src/trips.py | 20 ++- src/gtfs_vigo_stops/stop_report.py | 138 ++++++++++++++++++++- 5 files changed, 202 insertions(+), 65 deletions(-) (limited to 'src/gtfs_vigo_stops') 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, } ) -- cgit v1.3