Skip to content

Commit

Permalink
feat: add exception dates to carpool model
Browse files Browse the repository at this point in the history
  • Loading branch information
hbruch committed Feb 5, 2025
1 parent b90d77d commit ab6e715
Show file tree
Hide file tree
Showing 5 changed files with 80 additions and 7 deletions.
27 changes: 25 additions & 2 deletions amarillo/models/Carpool.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import time, date, datetime
from datetime import time, date as dt_date, datetime
from pydantic import ConfigDict, BaseModel, Field, HttpUrl, EmailStr
from typing import List, Union, Set, Optional, Tuple
from datetime import time
Expand All @@ -9,6 +9,7 @@
NumType = Union[float, int]

MAX_STOPS_PER_TRIP = 100
MAX_EXCEPTION_DATES = 90

class Weekday(str, Enum):
monday = "monday"
Expand All @@ -19,6 +20,10 @@ class Weekday(str, Enum):
saturday = "saturday"
sunday = "sunday"

class ExceptionType(str, Enum):
added = "added"
removed = "removed"

class PickupDropoffType(str, Enum):
pickup_and_dropoff = "pickup_and_dropoff"
only_pickup = "only_pickup"
Expand Down Expand Up @@ -182,6 +187,18 @@ class Agency(BaseModel):
#"""
})

class ExceptionDate(BaseModel):
date: dt_date = Field(
description="Date on which this ride is exceptionally offered or not offered.",
examples=["2025-01-01"])

exceptionType: ExceptionType = Field(
description="""Type of exception, i.e. if the offer is running
(though not specified by departureDate) or not
(even though it usually, according to departureDate,
runs on this weekday)""",
examples=["added", "removed"])

class Carpool(BaseModel):
id: str = Field(
description="ID of the carpool. Should be supplied and managed by the "
Expand Down Expand Up @@ -241,7 +258,7 @@ class Carpool(BaseModel):
examples=["17:00"])

# TODO think about using googlecal Format
departureDate: Union[date, Set[Weekday]] = Field(
departureDate: Union[dt_date, Set[Weekday]] = Field(
description="Date when the trip will start, in case it is a one-time "
"trip. For recurring trips, specify weekdays. "
"Note, that when for different weekdays different "
Expand All @@ -250,6 +267,12 @@ class Carpool(BaseModel):
examples=['A single date 2022-04-04 or a list of weekdays ["saturday", '
'"sunday"]'])

exceptionDates: Optional[List[ExceptionDate]] = Field(None,
description="List of exceptions to a weekly schedule. All dates of the list must be unique, no more than 90 days are accepted",
max_length=MAX_EXCEPTION_DATES,
examples=[{"date": "2025-01-01", "exceptionType": "removed"},
{"date": "2025-01-02", "exceptionType": "added"}])

path: Optional[LineString] = Field(
None, description="Optional route geometry as json LineString.")

Expand Down
16 changes: 16 additions & 0 deletions amarillo/services/gtfs_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,27 @@ def _prepare_gtfs_feed(self, ridestore, stopstore):
self._convert_trip(trip)

def _convert_trip(self, trip):
"""
Appends all required gtfs records for this trip to the
GTFS feeds files. I.e. it creates a new route, trip,
calendar, calendar_dates (in case it's no regular trip or
some dates are exceptionally served/unserved), stop_times
and shapes.
"""
self.routes_counter += 1
self.routes.append(self._create_route(trip))
self.calendar.append(self._create_calendar(trip))
if not trip.runs_regularly:
self.calendar_dates.append(self._create_calendar_date(trip))
else:
# Append exceptions from regular schedule to calendar_dates
if trip.additional_service_days and len(trip.additional_service_days) > 0:
calendar_dates = [GtfsCalendarDate(trip.trip_id, self._convert_stop_date(d), CALENDAR_DATES_EXCEPTION_TYPE_ADDED) for d in trip.additional_service_days]
self.calendar_dates.extend(calendar_dates)
if trip.non_service_days and len(trip.non_service_days) > 0:
calendar_dates = [GtfsCalendarDate(trip.trip_id, self._convert_stop_date(d), CALENDAR_DATES_EXCEPTION_TYPE_REMOVED) for d in trip.non_service_days]
self.calendar_dates.extend(calendar_dates)

self.trips.append(self._create_trip(trip, self.routes_counter))
self._append_stops_and_stop_times(trip)
self._append_shapes(trip, self.routes_counter)
Expand Down
16 changes: 12 additions & 4 deletions amarillo/services/trips.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

class Trip:

def __init__(self, trip_id, route_name, headsign, url, calendar, departureTime, path, agency, lastUpdated, stop_times, bbox):
def __init__(self, trip_id, route_name, headsign, url, calendar, departureTime, path, agency, lastUpdated, stop_times, bbox, additional_service_days, non_service_days):
if isinstance(calendar, set):
self.runs_regularly = True
self.weekdays = [
Expand Down Expand Up @@ -46,6 +46,8 @@ def __init__(self, trip_id, route_name, headsign, url, calendar, departureTime,
self.bbox = bbox
self.route_name = route_name
self.trip_headsign = headsign
self.additional_service_days = additional_service_days
self.non_service_days = non_service_days

def path_as_line_string(self):
return path
Expand All @@ -59,7 +61,7 @@ def start_time_str(self):
def next_trip_dates(self, start_date, day_count=14):
if self.runs_regularly:
for single_date in (start_date + timedelta(n) for n in range(day_count)):
if self.weekdays[single_date.weekday()]==1:
if self.weekdays[single_date.weekday()]==1 and single_date not in self.non_service_days or single_date in self.additional_service_days:
yield single_date.strftime("%Y%m%d")
else:
yield self.start.strftime("%Y%m%d")
Expand Down Expand Up @@ -217,8 +219,14 @@ def transform_to_trip(self, carpool):
min([pt[1] for pt in path.coordinates]),
max([pt[0] for pt in path.coordinates]),
max([pt[1] for pt in path.coordinates]))

trip = Trip(trip_id, route_name, headsign, str(carpool.deeplink), carpool.departureDate, carpool.departureTime, carpool.path, carpool.agency, carpool.lastUpdated, stop_times, bbox)

if carpool.exceptionDates is None:
additional_service_days = []
non_service_days = []
else:
additional_service_days = [exception.date for exception in carpool.exceptionDates if exception.exceptionType=='added']
non_service_days = [exception.date for exception in carpool.exceptionDates if exception.exceptionType=='removed']
trip = Trip(trip_id, route_name, headsign, str(carpool.deeplink), carpool.departureDate, carpool.departureTime, carpool.path, carpool.agency, carpool.lastUpdated, stop_times, bbox, additional_service_days, non_service_days)

return trip

Expand Down
14 changes: 14 additions & 0 deletions amarillo/tests/sampledata.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,20 @@
'departureDate': ["monday"],
}

carpool_with_exception_dates = {
'id': "carpool_with_exception_dates",
'agency': "mfdz",
'deeplink': "https://mfdz.de/trip/123",
'stops': [
{'id': "mfdz:12073:001", 'name': "abc", 'lat': 53.11901, 'lon': 14.015776},
{'id': "de:12073:900340137::3", 'name': "xyz", 'lat': 53.011459, 'lon': 13.94945}],
'departureTime': "15:00",
'departureDate': ["monday"],
'exceptionDates': [
{ "date": "2025-01-01", "exceptionType": "removed"},
{ "date": "2025-01-02", "exceptionType": "added"}
]
}

cp1 = Carpool(**data1)

Expand Down
14 changes: 13 additions & 1 deletion amarillo/tests/test_gtfs.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from amarillo.tests.sampledata import carpool_1234, data1, carpool_repeating_json, stop_issue
from amarillo.tests.sampledata import carpool_1234, data1, carpool_repeating_json, carpool_with_exception_dates, stop_issue
from amarillo.services.gtfs_export import GtfsExport
from amarillo.services.gtfs import GtfsRtProducer
from amarillo.services.stops import StopsStore
Expand All @@ -19,6 +19,18 @@ def test_gtfs_generation():
exporter = GtfsExport(None, None, trips_store, stops_store)
exporter.export('target/tests/test_gtfs_generation/test.gtfs.zip', "target/tests/test_gtfs_generation")

def test_gtfs_generation_with_exception_dates():
cp = Carpool(**carpool_with_exception_dates)
stops_store = StopsStore()
trips_store = TripStore(stops_store, AgencyConfService())
trips_store.put_carpool(cp)

exporter = GtfsExport(None, None, trips_store, stops_store)
exporter._prepare_gtfs_feed(trips_store, stops_store)
assert len(exporter.calendar_dates) == 2
assert exporter.calendar_dates[0].date == '20250102'
assert exporter.calendar_dates[0].exception_type == 1

def test_correct_stops():
cp = Carpool(**stop_issue)
stops_store = StopsStore([{"url": "https://datahub.bbnavi.de/export/rideshare_points.geojson", "vicinity": 250}])
Expand Down

0 comments on commit ab6e715

Please sign in to comment.