diff --git a/.gitignore b/.gitignore index 3f1a4846..2089d411 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,4 @@ __pycache__ env # Visual Studio Code workspace settings folder -.vscode \ No newline at end of file +.vscode diff --git a/osm2gtfs/core/elements.py b/osm2gtfs/core/elements.py index 9609e570..f63ad871 100644 --- a/osm2gtfs/core/elements.py +++ b/osm2gtfs/core/elements.py @@ -66,12 +66,13 @@ def __attrs_post_init__(self): known_route_types = { 'tram': 'Tram', - 'light_rail': 'Tram', + 'light_rail': 'Light Rail', 'subway': 'Subway', 'train': 'Rail', 'bus': 'Bus', 'trolleybus': 'Bus', - 'ferry': 'Ferry' + 'ferry': 'Ferry', + 'share_taxi': 'Share Taxi' } if self.route_type not in known_route_types: diff --git a/osm2gtfs/creators/et_addisababa/__init__.py b/osm2gtfs/creators/et_addisababa/__init__.py new file mode 100644 index 00000000..5c9136ad --- /dev/null +++ b/osm2gtfs/creators/et_addisababa/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python +# coding=utf-8 diff --git a/osm2gtfs/creators/et_addisababa/config.json b/osm2gtfs/creators/et_addisababa/config.json new file mode 100644 index 00000000..374460e8 --- /dev/null +++ b/osm2gtfs/creators/et_addisababa/config.json @@ -0,0 +1,35 @@ +{ + "query": { + "bbox": { + "n": "9.13", + "s": "8.8", + "e": "38.96", + "w": "38.61" + }, + "tags": { + "route": ["bus", "light_rail", "share_taxi"] + } + }, + "stops": { + "name_without": "no name", + "name_auto": "yes" + }, + "agency": { + "agency_id": "AA", + "agency_name": "Addis Ababa Transport (all)", + "agency_url": "https://example.com", + "agency_timezone": "Africa/Addis_Ababa", + "agency_lang": "en", + "agency_phone": "", + "agency_fare_url": "" + }, + "feed_info": { + "publisher_name": "AddisMap", + "publisher_url": "http://addismap.com", + "version": "0.1", + "start_date": "20191201", + "end_date": "20991231" + }, + "output_file": "data/et-addisababa.zip", + "selector": "et_addisababa" +} diff --git a/osm2gtfs/creators/et_addisababa/config_AB.json b/osm2gtfs/creators/et_addisababa/config_AB.json new file mode 100644 index 00000000..a1f7142a --- /dev/null +++ b/osm2gtfs/creators/et_addisababa/config_AB.json @@ -0,0 +1,36 @@ +{ + "query": { + "bbox": { + "n": "9.13", + "s": "8.8", + "e": "38.96", + "w": "38.61" + }, + "tags": { + "route": "bus", + "operator:short": "AB" + } + }, + "stops": { + "name_without": "no name", + "name_auto": "yes" + }, + "agency": { + "agency_id": "AB", + "agency_name": "Anbessa City Bus", + "agency_url": "https://example.com", + "agency_timezone": "Africa/Addis_Ababa", + "agency_lang": "en", + "agency_phone": "", + "agency_fare_url": "" + }, + "feed_info": { + "publisher_name": "AddisMap", + "publisher_url": "http://addismap.com", + "version": "0.1", + "start_date": "20190101", + "end_date": "20991231" + }, + "output_file": "data/et-addisababa-ab.zip", + "selector": "et_addisababa" +} diff --git a/osm2gtfs/creators/et_addisababa/config_SH.json b/osm2gtfs/creators/et_addisababa/config_SH.json new file mode 100644 index 00000000..b0e430a7 --- /dev/null +++ b/osm2gtfs/creators/et_addisababa/config_SH.json @@ -0,0 +1,36 @@ +{ + "query": { + "bbox": { + "n": "9.13", + "s": "8.8", + "e": "38.96", + "w": "38.61" + }, + "tags": { + "route": "bus", + "operator:short": "SH" + } + }, + "stops": { + "name_without": "no name", + "name_auto": "yes" + }, + "agency": { + "agency_id": "SH", + "agency_name": "Sheger City Bus", + "agency_url": "https://example.com", + "agency_timezone": "Africa/Addis_Ababa", + "agency_lang": "en", + "agency_phone": "", + "agency_fare_url": "" + }, + "feed_info": { + "publisher_name": "AddisMap", + "publisher_url": "http://addismap.com", + "version": "0.1", + "start_date": "20190101", + "end_date": "20991231" + }, + "output_file": "data/et-addisababa-sh.zip", + "selector": "et_addisababa" +} diff --git a/osm2gtfs/creators/et_addisababa/routes_creator_et_addisababa.py b/osm2gtfs/creators/et_addisababa/routes_creator_et_addisababa.py new file mode 100644 index 00000000..83c88f85 --- /dev/null +++ b/osm2gtfs/creators/et_addisababa/routes_creator_et_addisababa.py @@ -0,0 +1,13 @@ +# coding=utf-8 + +from osm2gtfs.creators.routes_creator import RoutesCreator + + +class RoutesCreatorEtAddisababa(RoutesCreator): + + def add_routes_to_feed(self, feed, data): + # Get routes information + data.get_routes() + + # GTFS routes are created in TripsCreator + return diff --git a/osm2gtfs/creators/et_addisababa/schedule_creator_et_addisababa.py b/osm2gtfs/creators/et_addisababa/schedule_creator_et_addisababa.py new file mode 100644 index 00000000..773bfb41 --- /dev/null +++ b/osm2gtfs/creators/et_addisababa/schedule_creator_et_addisababa.py @@ -0,0 +1,10 @@ +# coding=utf-8 + +from osm2gtfs.creators.schedule_creator import ScheduleCreator + + +class ScheduleCreatorEtAddisababa(ScheduleCreator): + + def add_schedule_to_data(self, data): + # Don't use any schedule source + data.schedule = None diff --git a/osm2gtfs/creators/et_addisababa/stops_creator_et_addisababa.py b/osm2gtfs/creators/et_addisababa/stops_creator_et_addisababa.py new file mode 100644 index 00000000..3021905d --- /dev/null +++ b/osm2gtfs/creators/et_addisababa/stops_creator_et_addisababa.py @@ -0,0 +1,13 @@ +# coding=utf-8 + +from osm2gtfs.creators.stops_creator import StopsCreator + +class StopsCreatorEtAddisababa(StopsCreator): + def _define_stop_id(self, stop): + """ + We always use the node ID in Addis Ababa because refs currently might contain duplicates + """ + + stop_id = stop.osm_type + "/" + str(stop.osm_id) + + return stop_id \ No newline at end of file diff --git a/osm2gtfs/creators/et_addisababa/trips_creator_et_addisababa.py b/osm2gtfs/creators/et_addisababa/trips_creator_et_addisababa.py new file mode 100644 index 00000000..82b920c6 --- /dev/null +++ b/osm2gtfs/creators/et_addisababa/trips_creator_et_addisababa.py @@ -0,0 +1,133 @@ +# coding=utf-8 + +from datetime import timedelta, datetime + +from osm2gtfs.creators.trips_creator import TripsCreator +from osm2gtfs.core.helper import Helper +from osm2gtfs.core.elements import Line + +from transitfeed.trip import Trip + + +def time_string_to_minutes(time_string): + (hours, minutes, seconds) = time_string.split(':') + return int(hours) * 60 + int(minutes) + +class TripsCreatorEtAddisababa(TripsCreator): + service_weekday = None + + def add_trips_to_feed(self, feed, data): + self.service_weekday = feed.GetDefaultServicePeriod() + self.service_weekday.SetStartDate( + self.config['feed_info']['start_date']) + self.service_weekday.SetEndDate(self.config['feed_info']['end_date']) + self.service_weekday.SetWeekdayService(True) + self.service_weekday.SetWeekendService(True) + + lines = data.routes + for route_ref, line in sorted(lines.iteritems()): + if not isinstance(line, Line): + continue + print("Generating schedule for line: " + route_ref) + + flex_flag = None + if 'route_master' in line.tags and line.tags['route_master'] == "light_rail": + route_type = "Tram" + route_suffix = " (Light Rail)" + elif 'route_master' in line.tags and line.tags['route_master'] == "share_taxi": + route_type = "Bus" + route_suffix = " (Minibus)" + flex_flag = 0 + else: + route_type = "Bus" + route_suffix = "" + + line_gtfs = feed.AddRoute( + short_name=str(line.route_id), + long_name=line.name + route_suffix, + # we change the route_long_name with the 'from' and 'to' tags + # of the last route as the route_master name tag contains + # the line code (route_short_name) + route_type=route_type, + route_id=line.osm_id) + line_gtfs.agency_id = feed.GetDefaultAgency().agency_id + line_gtfs.route_desc = "" + line_gtfs.route_color = "1779c2" + line_gtfs.route_text_color = "ffffff" + + route_index = 0 + itineraries = line.get_itineraries() + for a_route in itineraries: + trip_gtfs = line_gtfs.AddTrip(feed) + trip_gtfs.shape_id = self._add_shape_to_feed( + feed, a_route.osm_id, a_route) + trip_gtfs.direction_id = route_index % 2 + route_index += 1 + + if a_route.fr and a_route.to: + trip_gtfs.trip_headsign = a_route.to + line_gtfs.route_long_name = a_route.fr.decode( + 'utf8') + " ↔ ".decode( + 'utf8') + a_route.to.decode('utf8') + route_suffix + + DEFAULT_ROUTE_FREQUENCY = 30 + DEFAULT_TRAVEL_TIME = 120 + + frequency = None + + ROUTE_FREQUENCY = DEFAULT_ROUTE_FREQUENCY + + if "interval" in a_route.tags: + frequency = a_route.tags['interval'] + try: + ROUTE_FREQUENCY = time_string_to_minutes(frequency) + if not ROUTE_FREQUENCY > 0: + print("frequency is invalid for route_master " + str( + line.osm_id)) + except (ValueError, TypeError) as e: + print("frequency not a number for route_master " + str( + line.osm_id)) + + trip_gtfs.AddFrequency( + "05:00:00", "22:00:00", ROUTE_FREQUENCY * 60) + + if 'duration' in a_route.tags: + try: + TRAVEL_TIME = time_string_to_minutes(a_route.tags['duration']); + if not TRAVEL_TIME > 0: + print("travel_time is invalid for route " + str( + a_route.osm_id)) + TRAVEL_TIME = DEFAULT_TRAVEL_TIME + except (ValueError, TypeError) as e: + print("travel_time not a number / exception thrown for route with OSM ID " + str( + a_route.osm_id)) + TRAVEL_TIME = DEFAULT_TRAVEL_TIME + else: + TRAVEL_TIME = DEFAULT_TRAVEL_TIME + print("WARNING: No duration set --- Using default travel time for route with OSM ID " +str(a_route.osm_id)); + + for index_stop, a_stop in enumerate(a_route.stops): + stop_id = a_stop + departure_time = datetime(2008, 11, 22, 6, 0, 0) + + if index_stop == 0: + trip_gtfs.AddStopTime(feed.GetStop( + str(stop_id)), stop_time=departure_time.strftime( + "%H:%M:%S"), continuous_pickup = flex_flag, continuous_drop_off = flex_flag) + elif index_stop == len(a_route.stops) - 1: + departure_time += timedelta(minutes=TRAVEL_TIME) + trip_gtfs.AddStopTime(feed.GetStop( + str(stop_id)), stop_time=departure_time.strftime( + "%H:%M:%S"), continuous_pickup = flex_flag, continuous_drop_off = flex_flag) + else: + trip_gtfs.AddStopTime(feed.GetStop(str(stop_id)), continuous_pickup = flex_flag, continuous_drop_off = flex_flag) + + for secs, stop_time, is_timepoint in trip_gtfs.GetTimeInterpolatedStops(): + stop_time.continuous_pickup_flag = flex_flag + stop_time.continuous_drop_off_flag = flex_flag + if not is_timepoint: + stop_time.arrival_secs = secs + stop_time.departure_secs = secs + trip_gtfs.ReplaceStopTimeObject(stop_time) + + Helper.interpolate_stop_times(trip_gtfs) diff --git a/osm2gtfs/osm2gtfs.py b/osm2gtfs/osm2gtfs.py index c92a2f8f..e01e57cb 100644 --- a/osm2gtfs/osm2gtfs.py +++ b/osm2gtfs/osm2gtfs.py @@ -5,11 +5,15 @@ import sys import logging import argparse +import transitfeedflex import transitfeed from core.configuration import Configuration from core.osm_connector import OsmConnector from core.creator_factory import CreatorFactory +from transitfeed.trip import Trip +from transitfeed.gtfsfactoryuser import GtfsFactoryUser + # Define logging level logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) @@ -40,7 +44,6 @@ def main(): - # Load, prepare and validate configuration config = Configuration(args) @@ -62,8 +65,18 @@ def main(): data.get_stops(refresh=True) config.get_schedule_source(refresh=True) + gtfs_factory = transitfeedflex.GetGtfsFactory() + + def GetGtfsFactory(self): + return gtfs_factory; + + # Monkey patch all the base classes + GtfsFactoryUser.GetGtfsFactory = GetGtfsFactory + # Define (transitfeed) object for GTFS creation - feed = transitfeed.Schedule() + feed = transitfeedflex.FlexSchedule(gtfs_factory=gtfs_factory) + + # Initiate creators for GTFS components through an object factory factory = CreatorFactory(config) diff --git a/transitfeedflex/README b/transitfeedflex/README new file mode 100644 index 00000000..f3fcc69f --- /dev/null +++ b/transitfeedflex/README @@ -0,0 +1 @@ +Adds Flexible Pick Off / Drop Off (Part of the GTFSFlex specification) \ No newline at end of file diff --git a/transitfeedflex/__init__.py b/transitfeedflex/__init__.py new file mode 100644 index 00000000..7f5ba959 --- /dev/null +++ b/transitfeedflex/__init__.py @@ -0,0 +1,6 @@ +#!/usr/bin/python2.5 + +from __future__ import absolute_import +from .flexstoptime import * +from .flexschedule import * +from .setup_extension import * diff --git a/transitfeedflex/flexschedule.py b/transitfeedflex/flexschedule.py new file mode 100644 index 00000000..1b0097b5 --- /dev/null +++ b/transitfeedflex/flexschedule.py @@ -0,0 +1,11 @@ +from transitfeed.schedule import Schedule + +class FlexSchedule(Schedule): + + def ConnectDb(self, memory_db): + super(FlexSchedule, self).ConnectDb(memory_db) + cursor = self._connection.cursor() + cursor.execute("""ALTER TABLE stop_times ADD continuous_pickup INTEGER;""") + cursor.execute("""ALTER TABLE stop_times ADD continuous_drop_off INTEGER;""") + + diff --git a/transitfeedflex/flexstoptime.py b/transitfeedflex/flexstoptime.py new file mode 100644 index 00000000..aecabc22 --- /dev/null +++ b/transitfeedflex/flexstoptime.py @@ -0,0 +1,27 @@ +from transitfeed.stoptime import StopTime + +class FlexStopTime(StopTime): + + _OPTIONAL_FIELD_NAMES = StopTime._OPTIONAL_FIELD_NAMES + [ 'continuous_pickup', 'continuous_drop_off'] + + _FIELD_NAMES = StopTime._REQUIRED_FIELD_NAMES + _OPTIONAL_FIELD_NAMES + + _SQL_FIELD_NAMES = StopTime._SQL_FIELD_NAMES + [ 'continuous_pickup', 'continuous_drop_off'] + + __slots__ = StopTime.__slots__ + ('continuous_pickup_flag', 'continuous_drop_off_flag') + + def __init__(self, problems, stop, arrival_time=None, departure_time=None, stop_headsign=None, pickup_type=None, drop_off_type=None, shape_dist_traveled=None, arrival_secs=None, departure_secs=None, stop_time=None, stop_sequence=None, timepoint=None, continuous_pickup=None, continuous_drop_off=None): + super(FlexStopTime, self).__init__(problems, stop, arrival_time=arrival_time, departure_time=departure_time, stop_headsign=stop_headsign, pickup_type=pickup_type, drop_off_type=drop_off_type, shape_dist_traveled=shape_dist_traveled, arrival_secs=arrival_secs, departure_secs=departure_secs, stop_time=stop_time, stop_sequence=stop_sequence, timepoint=timepoint) + self.continuous_pickup_flag = continuous_pickup + self.continuous_drop_off_flag = continuous_drop_off + + def __getattr__(self, name): + if name == 'continuous_pickup': + if self.continuous_drop_off_flag == None: + return '' + return str(self.continuous_pickup_flag) # force 0 to be exported, because default = 1 = blank + elif name == 'continuous_drop_off': + if self.continuous_drop_off_flag == None: + return '' + return str(self.continuous_drop_off_flag) + return super(FlexStopTime, self).__getattr__(name) diff --git a/transitfeedflex/flextrip.py b/transitfeedflex/flextrip.py new file mode 100644 index 00000000..f4cbdc54 --- /dev/null +++ b/transitfeedflex/flextrip.py @@ -0,0 +1,37 @@ +from transitfeed.trip import Trip + +import transitfeed.problems as problems_module + +class FlexTrip(Trip): + def GetStopTimes(self, problems=None): + """Adds continuous_pickup + continuous_drop_off to fetching script""" + # In theory problems=None should be safe because data from database has been + # validated. See comment in _LoadStopTimes for why this isn't always true. + cursor = self._schedule._connection.cursor() + cursor.execute( + 'SELECT arrival_secs,departure_secs,stop_headsign,pickup_type,' + 'drop_off_type,shape_dist_traveled,stop_id,stop_sequence,timepoint, continuous_pickup, continuous_drop_off ' + 'FROM stop_times ' + 'WHERE trip_id=? ' + 'ORDER BY stop_sequence', (self.trip_id,)) + stop_times = [] + stoptime_class = self.GetGtfsFactory().StopTime + if problems is None: + # TODO: delete this branch when StopTime.__init__ doesn't need a + # ProblemReporter + problems = problems_module.default_problem_reporter + for row in cursor.fetchall(): + stop = self._schedule.GetStop(row[6]) + stop_times.append(stoptime_class(problems=problems, + stop=stop, + arrival_secs=row[0], + departure_secs=row[1], + stop_headsign=row[2], + pickup_type=row[3], + drop_off_type=row[4], + shape_dist_traveled=row[5], + stop_sequence=row[7], + timepoint=row[8], + continuous_pickup=row[9], + continuous_drop_off=row[10])) + return stop_times diff --git a/transitfeedflex/setup_extension.py b/transitfeedflex/setup_extension.py new file mode 100644 index 00000000..ba8d068a --- /dev/null +++ b/transitfeedflex/setup_extension.py @@ -0,0 +1,16 @@ +#!/usr/bin/python2 + +from __future__ import absolute_import +import transitfeed + +from . import flexstoptime +from . import flextrip + +def GetGtfsFactory(factory = None): + if not factory: + factory = transitfeed.GetGtfsFactory() + + factory.UpdateClass('StopTime', flexstoptime.FlexStopTime) + factory.UpdateClass('Trip', flextrip.FlexTrip) + + return factory