Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Addis Ababa and Add Partial GTFSFlex Implementation (#156) #159

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ __pycache__
env

# Visual Studio Code workspace settings folder
.vscode
.vscode
5 changes: 3 additions & 2 deletions osm2gtfs/core/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions osm2gtfs/creators/et_addisababa/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python
# coding=utf-8
35 changes: 35 additions & 0 deletions osm2gtfs/creators/et_addisababa/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All config should come with some regression tests, so we can make sure our future changes in osm2gtfs do not break your own implementation.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will probably not be able to add those from my current limited python skills. Help is appreciated.

"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"
}
36 changes: 36 additions & 0 deletions osm2gtfs/creators/et_addisababa/config_AB.json
Original file line number Diff line number Diff line change
@@ -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"
}
36 changes: 36 additions & 0 deletions osm2gtfs/creators/et_addisababa/config_SH.json
Original file line number Diff line number Diff line change
@@ -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"
}
13 changes: 13 additions & 0 deletions osm2gtfs/creators/et_addisababa/routes_creator_et_addisababa.py
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions osm2gtfs/creators/et_addisababa/schedule_creator_et_addisababa.py
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions osm2gtfs/creators/et_addisababa/stops_creator_et_addisababa.py
Original file line number Diff line number Diff line change
@@ -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
133 changes: 133 additions & 0 deletions osm2gtfs/creators/et_addisababa/trips_creator_et_addisababa.py
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why you need to override the route_type here 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In OSM we tagged it as light_rail, and I was thinking it needs to be mapped to Tram in GTFS; but I am not sure

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if flex_flag is a flag, can we use a Boolean ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably yes :-)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the idea of it being hard-coded. Can we rather have that information as an attribute on the route / route_master in OSM ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possible, need to discuss with the OSM community how it is mapped best on OSM side.

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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the ci_abidjan creator also use duration and interval tags for the same purpose 🤔 I wonder if / how we can mutualize the implementations to avoid code duplication.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, how could we do this?

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)
17 changes: 15 additions & 2 deletions osm2gtfs/osm2gtfs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down Expand Up @@ -40,7 +44,6 @@


def main():

# Load, prepare and validate configuration
config = Configuration(args)

Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions transitfeedflex/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Adds Flexible Pick Off / Drop Off (Part of the GTFSFlex specification)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 That's really cool to add this feature!

Can you please add a link to the documentation of the format ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

long time no feedback, sorry :-)
I think it is described here: https://github.com/MobilityData/gtfs-flex/blob/master/spec/reference.md

6 changes: 6 additions & 0 deletions transitfeedflex/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/python2.5

from __future__ import absolute_import
from .flexstoptime import *
from .flexschedule import *
from .setup_extension import *
11 changes: 11 additions & 0 deletions transitfeedflex/flexschedule.py
Original file line number Diff line number Diff line change
@@ -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;""")


Loading