Skip to content

Commit

Permalink
Factory based abstracted schedule creation
Browse files Browse the repository at this point in the history
Relates to #9
  • Loading branch information
Felix authored and grote committed Oct 2, 2016
1 parent a437197 commit c8f76c8
Show file tree
Hide file tree
Showing 17 changed files with 803 additions and 564 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*.zip
*.pkl
config.json
data

# Android Studio files
.idea/*
Expand Down
45 changes: 45 additions & 0 deletions creators/AgencyCreator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# coding=utf-8

import sys
import transitfeed

class AgencyCreator(object):

def __init__(self, config):
self.config = config

def __repr__(self):
rep = ""
if self.config is not None:
rep += str(self.config) + " | "
return rep

def add_agency_to_schedule(self, schedule):
schedule.AddAgencyObject(self.prepare_agency())

def prepare_agency(self):
"""
Loads agency data from a json config file. The valid keys under the json
"agency" object correspond to the transitfeed.Agency field names.
Return a transitfeed.Agency object
"""
fields = ['agency_name', 'agency_url', 'agency_timezone', 'agency_id', 'agency_lang',
'agency_phone', 'agency_fare_url']
data_dict = {}
for field_name in fields:
if field_name in self.config["agency"]:
field_value = self.config["agency"][field_name]
if field_value is not None and field_value != "":
data_dict[field_name] = field_value
else:
sys.stderr.write("Warning: Key '" + field_name + "' was not found in the config file.")

agency = transitfeed.Agency(field_dict=data_dict)

if not agency.Validate():
sys.stderr.write("Error: Agency data is invalid.")
# TODO error handling based on a exception
# raise AttributeError('Agency data in config file in not valid.')

return agency
32 changes: 32 additions & 0 deletions creators/FeedInfoCreator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# coding=utf-8

import sys
import transitfeed

class FeedInfoCreator(object):

def __init__(self, config):
self.config = config

def __repr__(self):
rep = ""
if self.config is not None:
rep += str(self.config) + " | "
return rep

def add_feed_info_to_schedule(self, schedule):
schedule.AddFeedInfoObject(self.prepare_feed_info())

def prepare_feed_info(self):
"""
Loads feed info data from a json config file.
Return a transitfeed.FeedInfo object
"""
feed_info = transitfeed.FeedInfo()
feed_info.feed_publisher_name = self.config['feed_info']['publisher_name']
feed_info.feed_publisher_url = self.config['feed_info']['publisher_url']
feed_info.feed_lang = self.config['agency']['agency_lang']
feed_info.feed_start_date = self.config['feed_info']['start_date']
feed_info.feed_end_date = self.config['feed_info']['end_date']
feed_info.feed_version = self.config['feed_info']['version']
return feed_info
9 changes: 9 additions & 0 deletions creators/Fenix/RoutesCreatorFenix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# coding=utf-8

from creators.RoutesCreator import RoutesCreator

class RoutesCreatorFenix(RoutesCreator):

def add_routes_to_schedule(self, schedule, data):
# Fenix logic is doing route aggregation in TripsCreator
return
19 changes: 19 additions & 0 deletions creators/Fenix/StopsCreatorFenix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# coding=utf-8

import sys
from creators.StopsCreator import StopsCreator

class StopsCreatorFenix(StopsCreator):

def add_stops_to_schedule(self, schedule, data):

stops = data.stops

# add all stops to GTFS
for stop in stops.values():
schedule.AddStop(
lat=float(stop.lat),
lng=float(stop.lon),
name=stop.name,
stop_id=str(stop.id)
)
203 changes: 203 additions & 0 deletions creators/Fenix/TripsCreatorFenix.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# coding=utf-8

import sys
import json
import transitfeed
from datetime import datetime
from creators.TripsCreator import TripsCreator
from osmhelper.osm_routes import Route, RouteMaster

DEBUG_ROUTE = "104"

WEEKDAY = "Dias Úteis"
SATURDAY = "Sábado"
SUNDAY = "Domingo"

class TripsCreatorFenix(TripsCreator):

def add_trips_to_schedule(self, schedule, data):

service_weekday = schedule.NewDefaultServicePeriod()
service_weekday.SetStartDate(self.config['feed_info']['start_date'])
service_weekday.SetEndDate(self.config['feed_info']['end_date'])
service_weekday.SetWeekdayService(True)
service_weekday.SetWeekendService(False)

service_saturday = schedule.NewDefaultServicePeriod()
service_saturday.SetStartDate(self.config['feed_info']['start_date'])
service_saturday.SetEndDate(self.config['feed_info']['end_date'])
service_saturday.SetWeekdayService(False)
service_saturday.SetWeekendService(False)
service_saturday.SetDayOfWeekHasService(5, True)

service_sunday = schedule.NewDefaultServicePeriod()
service_sunday.SetStartDate(self.config['feed_info']['start_date'])
service_sunday.SetEndDate(self.config['feed_info']['end_date'])
service_sunday.SetWeekdayService(False)
service_sunday.SetWeekendService(False)
service_sunday.SetDayOfWeekHasService(6, True)

routes = data.routes

# Get Fenix data from JSON file
json_data = []
with open('data/linhas.json') as f:
for line in f:
json_data.append(json.loads(line))
linhas = json_data[0]['data']

blacklist = ['10200', '12400', '328', '466', '665']
# Try to find OSM routes in Fenix data
for route_ref, route in sorted(routes.iteritems()):
found = False
for linha in linhas:
if route_ref == linha:
route.add_linha(linhas[linha])
if isinstance(route, RouteMaster):
for sub_route in route.routes.values():
sub_route.add_linha(linhas[linha])
found = True
break

if not found and route_ref not in blacklist:
sys.stderr.write("Route not found in Fenix data: " + str(route) + "\n")
blacklist.append(route_ref)

# delete missing routes from OSM data
for route_ref in blacklist:
if route_ref in routes:
del routes[route_ref]

# add trips for all routes
for route_ref, route in sorted(routes.iteritems()):
line = schedule.AddRoute(
short_name=route.ref,
long_name=route.name,
route_type="Bus")
line.agency_id = schedule.GetDefaultAgency().agency_id
line.route_desc = "TEST DESCRIPTION"
line.route_url = "http://www.consorciofenix.com.br/horarios?q=" + str(route.ref)
line.route_color = "1779c2"
line.route_text_color = "ffffff"

weekday = {}
saturday = {}
sunday = {}

for day in route.horarios:
sday = day.encode('utf-8')

if sday.startswith(WEEKDAY):
weekday[sday.replace(WEEKDAY + ' - Saída ', '')] = route.horarios[day]
elif sday.startswith(SATURDAY):
saturday[sday.replace(SATURDAY + ' - Saída ', '')] = route.horarios[day]
elif sday.startswith(SUNDAY):
sunday[sday.replace(SUNDAY + ' - Saída ', '')] = route.horarios[day]
else:
raise RuntimeError("Unknown day in Fenix data: " + day)

self.add_trips_by_day(schedule, line, service_weekday, route, weekday, WEEKDAY)
self.add_trips_by_day(schedule, line, service_saturday, route, saturday, SATURDAY)
self.add_trips_by_day(schedule, line, service_sunday, route, sunday, SUNDAY)

def add_trips_by_day(self, schedule, line, service, route, horarios, day):
# check if we even have service
if horarios is None or len(horarios) == 0:
return

if isinstance(route, RouteMaster):
# recurse into "Ida" and "Volta" routes
for sub_route in route.routes.values():
self.add_trips_by_day(schedule, line, service, sub_route, horarios, day)
return

# have at least two stops
if len(route.stops) < 2:
sys.stderr.write("Skipping Route, has no stops: " + str(route) + "\n")
return

# check if we have a match for the first stop
key = route.match_first_stops(horarios.keys())

if key is None:
# Do not print debug output here, because already done in route.match_first_stops()
return

if route.ref == DEBUG_ROUTE:
print "\n\n\n" + str(route)
print day + " - " + key

# get shape id
shape_id = str(route.id)
try:
schedule.GetShape(shape_id)
except KeyError:
shape = transitfeed.Shape(shape_id)
for point in route.shape:
shape.AddPoint(lat=float(point["lat"]), lon=float(point["lon"]))
schedule.AddShapeObject(shape)

if len(horarios) > 1 and not route.has_proper_master():
sys.stderr.write("Route should have a master: " + str(route) + "\n")

for time_group in horarios[key]:
for time_point in time_group:
# parse first departure time
start_time = datetime.strptime(time_point[0], "%H:%M")
start_time = str(start_time.time())

# calculate last arrival time for GTFS
start_sec = transitfeed.TimeToSecondsSinceMidnight(start_time)
factor = 1
if len(horarios) > 1 and not route.has_proper_master():
# since this route has only one instead of two trips, double the duration
factor = 2
end_sec = start_sec + route.duration.seconds * factor
end_time = transitfeed.FormatSecondsSinceMidnight(end_sec)

# save options
opts = time_point[1]

trip = line.AddTrip(schedule, headsign=route.name, service_period=service)
# add empty attributes to make navitia happy
trip.block_id = ""
trip.wheelchair_accessible = ""
trip.bikes_allowed = ""
trip.shape_id = shape_id
trip.direction_id = ""
if route.ref == DEBUG_ROUTE:
print "ADD TRIP " + str(trip.trip_id) + ":"
self.add_trip_stops(schedule, trip, route, start_time, end_time, opts)

# interpolate times, because Navitia can not handle this itself
self.interpolate_stop_times(trip)


def add_trip_stops(self, schedule, trip, route, start_time, end_time, opts):

if isinstance(route, Route):
i = 1
for stop in route.stops:
if i == 1:
# timepoint="1" (Times are considered exact)
trip.AddStopTime(schedule.GetStop(str(stop.id)), stop_time=start_time)
if route.ref == DEBUG_ROUTE:
print "START: " + start_time + " at " + str(stop)
elif i == len(route.stops):
# timepoint="0" (Times are considered approximate)
trip.AddStopTime(schedule.GetStop(str(stop.id)), stop_time=end_time)
if route.ref == DEBUG_ROUTE:
print "END: " + end_time + " at " + str(stop)
else:
# timepoint="0" (Times are considered approximate)
trip.AddStopTime(schedule.GetStop(str(stop.id)))
# print "INTER: " + str(stop)
i += 1


def interpolate_stop_times(self, trip):
for secs, stop_time, is_timepoint in trip.GetTimeInterpolatedStops():
if not is_timepoint:
stop_time.arrival_secs = secs
stop_time.departure_secs = secs
trip.ReplaceStopTimeObject(stop_time)
2 changes: 2 additions & 0 deletions creators/Fenix/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python
# coding=utf-8
17 changes: 17 additions & 0 deletions creators/RoutesCreator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# coding=utf-8

import sys

class RoutesCreator(object):

def __init__(self, config):
self.config = config

def __repr__(self):
rep = ""
if self.config is not None:
rep += str(self.config) + " | "
return rep

def add_routes_to_schedule(self, schedule):
raise NotImplementedError( "Should have implemented this" )
17 changes: 17 additions & 0 deletions creators/StopsCreator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# coding=utf-8

import sys

class StopsCreator(object):

def __init__(self, config):
self.config = config

def __repr__(self):
rep = ""
if self.config is not None:
rep += str(self.config) + " | "
return rep

def add_stops_to_schedule(self, schedule, data):
raise NotImplementedError( "Should have implemented this" )
17 changes: 17 additions & 0 deletions creators/TripsCreator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# coding=utf-8

import sys

class TripsCreator(object):

def __init__(self, config):
self.config = config

def __repr__(self):
rep = ""
if self.config is not None:
rep += str(self.config) + " | "
return rep

def add_trips_to_schedule(self, schedule, data):
raise NotImplementedError( "Should have implemented this" )
2 changes: 2 additions & 0 deletions creators/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python
# coding=utf-8
Loading

0 comments on commit c8f76c8

Please sign in to comment.