require "pry"
require "sqlite3"
+require "date_utils"
+require "database"
+
class Gtfs
+ MULTI_PLATFORM_STOPS = {
+ "BARRHAVEN CENTRE" => ["1A", "2A"],
+ "BASELINE" => ["1A", "1B", "1C", "1D", "2A", "2B", "STN OFF ONLY"],
+ "BAYSHORE" => ["1A", "2A", "3A", "4A", "4B", "A", "B", "C"],
+ "BAYVIEW" => ["A", "B", "O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "BEATRICE" => ["1A", "2A"],
+ "BILLINGS BRIDGE" => ["1A", "2A", "3A", "3B", "3C", "4B", "4C", "4D"],
+ "BLAIR" => ["A", "B", "C", "D", "E", "H", "I", "O-TRAIN", "O-TRAIN WEST / OUEST"],
+ "CHAPEL HILL" => ["A", "B"],
+ "CYRVILLE" => ["A", "B", "O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "DOMINION" => ["1A", "2A", "STN OFF ONLY"],
+ "FALLOWFIELD" => ["1A", "2A"],
+ "GREENBORO" => ["1A", "1B", "2A"],
+ "HERON" => ["1A", "2A", "3A", "4A"],
+ "HURDMAN" => ["A", "B", "C", "D", "E", "O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "INNOVATION" => ["A", "B"],
+ "IRIS" => ["1A", "2A", "STN OFF ONLY"],
+ "JEANNE D'ARC" => ["1A", "1B", "2A", "3A", "3B", "4A", "4B"],
+ "LEES" => ["A", "B", "O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "LEITRIM" => ["1A", "2A"],
+ "LINCOLN FIELDS" => ["1A", "1B", "2A", "3A", "4A", "4B", "5A", "OFF ONLY"],
+ "LONGFIELDS" => ["1A", "2A"],
+ "LYCÉE CLAUDEL" => ["1A", "2A"],
+ "LYON" => ["A", "B", "C", "O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "MACKENZIE KING" => ["1A", "2A"],
+ "MARKETPLACE" => ["1A", "2A"],
+ "MILLENNIUM" => ["1A", "2A"],
+ "NEPEAN WOODS" => ["1A", "2A"],
+ "PARLIAMENT / PARLEMENT" => ["A", "B", "C", "O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "PIMISI" => ["A", "B", "C", "D", "O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "PINECREST" => ["1A", "2A", "A", "B", "C"],
+ "PLEASANT PARK" => ["1A", "2A", "3A", "4A"],
+ "QUEENSWAY" => ["1A", "2A", "3A", "4A"],
+ "RIDEAU" => ["A", "B", "C", "O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "RIVERSIDE" => ["1A", "2A"],
+ "RIVERVIEW" => ["1A", "2A"],
+ "SMYTH" => ["1A", "2A"],
+ "SOUTH KEYS" => ["1A", "1B", "1C", "1D", "2A"],
+ "STRANDHERD" => ["1A", "2A"],
+ "ST-LAURENT" => ["A", "B", "C", "D", "O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "TERON" => ["1A", "2A"],
+ "TREMBLAY" => ["O-TRAIN EAST / EST", "O-TRAIN WEST / OUEST"],
+ "TRIM" => ["1A", "2A"],
+ "TUNNEY'S PASTURE" => ["A", "B", "C", "D", "E", "F", "G", "O-TRAIN", "O-TRAIN EAST / EST"],
+ "WALKLEY" => ["1A", "2A", "3A", "4A", "4B", "4C"],
+ "WESTBORO" => ["1A", "1B", "2A", "3A", "3A (A)", "4A", "A", "B", "OFF ONLY", "STN OFF ONLY"],
+ }
+
def initialize
@calendar = nil
+ @date_utils = DateUtils.new
+ @db = Database.new.db
end
def active_schedules(date_hash: nil)
- target_date = date_hash || today
+ target_date = date_hash || @date_utils.today
base_schedules = calendar_schedules(target_date)
overrides = calendar_overrides(target_date)
base_schedules + overrides[:add] - overrides[:remove]
end
- def trips_for_stop(stop_code:, date_hash: nil)
+ def trips(stop_code: nil, stop_id: nil, stop_name: nil, date_hash: nil, originating_only: false)
+ args = [stop_code, stop_id, stop_name].compact
+ raise "Must specify exactly one of stop_code, stop_id, or stop_name" if args.count != 1
+
+ if !stop_code.nil?
+ trips_for_stop_code(stop_code: stop_code, date_hash: date_hash, originating_only: originating_only)
+ elsif !stop_id.nil?
+ trips_for_stop_ids(stop_ids: [stop_id], date_hash: date_hash, originating_only: originating_only)
+ else
+ trips_for_stop_name(stop_name: stop_name, date_hash: date_hash, originating_only: originating_only)
+ end
+ end
+
+ private
+
+ def trips_for_stop_code(stop_code:, date_hash:)
+ stop_ids = @db.query("SELECT stop_id FROM stops WHERE stop_code=?", stop_code).to_a.map do |row|
+ row.first
+ end
+
+ trips_for_stop_ids(stop_ids: stop_ids, date_hash: date_hash)
+ end
+
+ def trips_for_stop_ids(stop_ids:, date_hash:, originating_only: false)
result = {}
schedules = active_schedules(date_hash: date_hash)
schedule_placeholders = (["?"] * schedules.count).join(",")
- stop_ids = db.query("SELECT stop_id FROM stops WHERE stop_code=?", stop_code).to_a.map do |row|
- row.first
- end
stop_id_placeholders = (["?"] * stop_ids.count).join(",")
- sql = <<~EOS
- SELECT r.route_short_name,t.trip_headsign,t.direction_id,st.departure_time
+ sql_head = <<~EOS
+ SELECT r.route_short_name,t.trip_headsign,t.direction_id,st.departure_time,t.trip_id
FROM routes r
INNER JOIN trips t ON t.route_id=r.route_id
INNER JOIN stop_times st ON st.trip_id=t.trip_id
WHERE st.stop_id IN (#{stop_id_placeholders})
AND service_id IN (#{schedule_placeholders})
+ EOS
+
+ sql_filter = if originating_only
+ "AND st.stop_sequence=1\n"
+ else
+ ""
+ end
+
+ sql_tail = <<~EOS
ORDER BY r.route_short_name,st.departure_time;
EOS
- db.query(sql, stop_ids, schedules).each do |row|
+ sql = "#{sql_head}#{sql_filter}#{sql_tail}"
+
+ @db.query(sql, stop_ids, schedules).each do |row|
route = row[0..2] # [route_id, hedsign, direction_id]
departure_time = row[3]
+ trip_id = row[4]
if result[route].nil?
result[route] = []
end
- result[route] << departure_time
+ result[route] << [departure_time, trip_id]
end
# Sometimes OC Transpo lists the same departure under two separate schedules, *both* of which are
# simulteneously in effect. (?)
# Here we compensate for that by eliminating duplicates of the same departure time.
- result.map do |route, departures|
- [route, departures.uniq]
+ result.map do |route, departure_tuples|
+ [
+ route,
+ departure_tuples.group_by do |tuple|
+ tuple.first # departure time
+ end.map do |group_key, group_members|
+ group_members.first
+ end
+ ]
end.to_h
end
- private
+ def trips_for_stop_name(stop_name:, date_hash:)
+ result = {}
+
+ base_name = stop_name.upcase
+ suffixes = MULTI_PLATFORM_STOPS[base_name] || [nil]
+ names = suffixes.map { |suffix| [base_name, suffix].compact.join(" ") }
+ placeholders = (["?"] * names.count).join(",")
+
+ stop_ids = @db.query(
+ "SELECT stop_id FROM stops WHERE stop_name IN (#{placeholders})",
+ names,
+ ).to_a.map do |row|
+ row.first
+ end
+
+ trips_for_stop_ids(stop_ids: stop_ids, date_hash: date_hash)
+ end
def calendar
@calendar ||= begin
FROM calendar
ORDER BY start_date, end_date
EOS
- db.query(sql).to_a
+ @db.query(sql).to_a
end
end
def calendar_overrides(target_date)
result = {add: [], remove: []}
- datestamp = date_for(target_date)
+ datestamp = @date_utils.date_for(target_date)
sql = <<~EOS
SELECT service_id, exception_type
FROM calendar_dates
WHERE calendar_date = ?
EOS
- db.query(sql).each do |row|
+ @db.query(sql).each do |row|
case row[1]
when "1"
result[:add] << row[0]
def calendar_schedules(target_date)
result = []
- datestamp = date_for(target_date)
- weekday = weekday_for(target_date)
+ datestamp = @date_utils.date_for(target_date)
+ weekday = @date_utils.weekday_for(target_date)
calendar.each do |service_id, mon, tue, wed, thu, fri, sat, sun, start_date, end_date|
if datestamp >= start_date && datestamp <= end_date
if (0 == weekday && "1" == sun) ||
end
result
end
-
- def date_for(date_hash)
- "#{date_hash[:year]}#{date_hash[:month].to_s.rjust(2, "0")}#{date_hash[:day].to_s.rjust(2, "0")}"
- end
-
- def db
- @db ||= SQLite3::Database.new("gtfs.db")
- end
-
- def today
- # Beware: This assumes your system is in America/Toronto time.
- now = Date.today
- {
- year: now.year,
- month: now.month,
- day: now.day,
- }
- end
-
- def weekday_for(date_hash)
- Date.new(date_hash[:year], date_hash[:month], date_hash[:day]).wday
- end
end