module SunbusGtfs
Public Instance Methods
Source
# File lib/sunbus/gtfs.rb, line 335 def best_trip_between(from_stop:, to_stop:, region:, fetch:, time:) data = feed(region: region, fetch: fetch, time: time) date = local_date(time) now_sec = (date.hour * 3600) + (date.min * 60) + date.sec best = nil data[:trips].each_value do |trip| next unless service_active?(trip[:service_id], date, data) times = data[:stop_times][trip[:trip_id]] from_row = times.find { |row| row[:stop_id] == from_stop[:stop_id] } to_row = times.find { |row| row[:stop_id] == to_stop[:stop_id] } next unless from_row && to_row next unless from_row[:stop_sequence] < to_row[:stop_sequence] depart_sec = parse_gtfs_clock(from_row[:departure_time]) arrive_sec = parse_gtfs_clock(to_row[:arrival_time]) next if depart_sec < now_sec - 60 route = data[:routes][trip[:route_id]] || {} candidate = { from_stop: from_stop, to_stop: to_stop, route_short_name: route[:route_short_name].to_s, route_long_name: route[:route_long_name].to_s, headsign: trip[:headsign], depart: format_gtfs_clock(depart_sec), arrive: format_gtfs_clock(arrive_sec), duration_min: ((arrive_sec - depart_sec) / 60.0).round, depart_sec: depart_sec } best = candidate if best.nil? || candidate[:depart_sec] < best[:depart_sec] end best end
Source
# File lib/sunbus/gtfs.rb, line 104 def build_feed(tables) stops = csv_rows(tables["stops"]).map do |row| { stop_id: row["stop_id"].to_s.strip, stop_code: row["stop_code"].to_s.strip, name: row["stop_name"].to_s.strip, lat: row["stop_lat"].to_s.strip, lon: row["stop_lon"].to_s.strip } end routes = {} csv_rows(tables["routes"]).each do |row| route_id = row["route_id"].to_s.strip routes[route_id] = { route_id: route_id, route_short_name: row["route_short_name"].to_s.strip, route_long_name: row["route_long_name"].to_s.strip } end trips = {} csv_rows(tables["trips"]).each do |row| trip_id = row["trip_id"].to_s.strip trips[trip_id] = { trip_id: trip_id, route_id: row["route_id"].to_s.strip, service_id: row["service_id"].to_s.strip, headsign: row["trip_headsign"].to_s.strip } end stop_times = Hash.new { |hash, key| hash[key] = [] } csv_rows(tables["stop_times"]).each do |row| trip_id = row["trip_id"].to_s.strip stop_times[trip_id] << { stop_id: row["stop_id"].to_s.strip, arrival_time: row["arrival_time"].to_s.strip, departure_time: row["departure_time"].to_s.strip, stop_sequence: row["stop_sequence"].to_i } end stop_times.each_value { |rows| rows.sort_by! { |row| row[:stop_sequence] } } calendar = {} csv_rows(tables["calendar"]).each do |row| service_id = row["service_id"].to_s.strip calendar[service_id] = { monday: row["monday"].to_i == 1, tuesday: row["tuesday"].to_i == 1, wednesday: row["wednesday"].to_i == 1, thursday: row["thursday"].to_i == 1, friday: row["friday"].to_i == 1, saturday: row["saturday"].to_i == 1, sunday: row["sunday"].to_i == 1, start_date: row["start_date"].to_s.strip, end_date: row["end_date"].to_s.strip } end calendar_dates = Hash.new { |hash, key| hash[key] = [] } csv_rows(tables["calendar_dates"]).each do |row| service_id = row["service_id"].to_s.strip next if service_id.empty? calendar_dates[service_id] << { date: row["date"].to_s.strip, exception_type: row["exception_type"].to_i } end { stops: stops, routes: routes, trips: trips, stop_times: stop_times, calendar: calendar, calendar_dates: calendar_dates } end
Source
# File lib/sunbus/gtfs.rb, line 30 def cached_zip_path(region) code = SunbusConfig.normalize_region(region) || SunbusConfig::DEFAULT_REGION File.join(SunbusConfig::GTFS_CACHE_DIR, "#{code.upcase}_GTFS.zip") end
Source
# File lib/sunbus/gtfs.rb, line 98 def csv_rows(table) return [] if table.nil? table.is_a?(CSV::Table) ? table : Array(table) end
Source
# File lib/sunbus/gtfs.rb, line 256 def departures_at_stop(stop, region: SunbusConfig::DEFAULT_REGION, fetch: nil, time: Time.now, limit: SunbusConfig::MAX_DEPARTURES) return [] unless stop data = feed(region: region, fetch: fetch, time: time) date = local_date(time) now_sec = (date.hour * 3600) + (date.min * 60) + date.sec rows = [] data[:trips].each_value do |trip| next unless service_active?(trip[:service_id], date, data) stop_row = data[:stop_times][trip[:trip_id]].find { |row| row[:stop_id] == stop[:stop_id] } next unless stop_row depart_sec = parse_gtfs_clock(stop_row[:departure_time]) next if depart_sec < now_sec - 60 route = data[:routes][trip[:route_id]] || {} rows << { route_short_name: route[:route_short_name].to_s, route_long_name: route[:route_long_name].to_s, headsign: trip[:headsign], departure_time: format_gtfs_clock(depart_sec), depart_sec: depart_sec, stop_id: stop[:stop_id] } end rt_rows = realtime_departures(stop[:stop_id], region: region, fetch: fetch, time: time) merged = merge_departures(rows, rt_rows).sort_by { |row| row[:depart_sec] } merged.first(limit) end
Source
# File lib/sunbus/gtfs.rb, line 58 def empty_feed { stops: [], routes: {}, trips: {}, stop_times: {}, calendar: {}, calendar_dates: {} } end
Source
# File lib/sunbus/gtfs.rb, line 41 def ensure_feed!(code, fetch: nil, time: Time.now) @feeds ||= {} @loaded_at ||= {} loaded = @loaded_at[code] return @feeds[code] if @feeds[code] && loaded && (time - loaded) < SunbusConfig::GTFS_CACHE_MAX_AGE_SEC path = cached_zip_path(code) refresh_zip!(path, region: code, fetch: fetch, time: time) unless zip_fresh?(path, time: time) @feeds[code] = parse_zip(path) @loaded_at[code] = time @feeds[code] rescue StandardError @feeds ||= {} @feeds[code] = empty_feed @feeds[code] end
Source
# File lib/sunbus/gtfs.rb, line 35 def feed(region:, fetch: nil, time: Time.now) code = SunbusConfig.normalize_region(region) || SunbusConfig::DEFAULT_REGION ensure_feed!(code, fetch: fetch, time: time) @feeds[code] end
Source
# File lib/sunbus/gtfs.rb, line 194 def find_stop(query, region: SunbusConfig::DEFAULT_REGION, fetch: nil) search_stops(query, region: region, fetch: fetch, limit: 1).first end
Source
# File lib/sunbus/gtfs.rb, line 246 def format_gtfs_clock(seconds) hour = seconds / 3600 minute = (seconds % 3600) / 60 format("%02d:%02d", hour, minute) end
Source
# File lib/sunbus/gtfs.rb, line 252 def local_date(time) time.getlocal("+10:00") end
Source
# File lib/sunbus/gtfs.rb, line 323 def merge_departures(scheduled, realtime) return scheduled if realtime.empty? seen = scheduled.map { |row| [row[:route_short_name], row[:departure_time]] }.to_set scheduled + realtime.reject { |row| seen.include?([row[:route_short_name], row[:departure_time]]) } end
Source
# File lib/sunbus/gtfs.rb, line 290 def next_departures(query, region: SunbusConfig::DEFAULT_REGION, fetch: nil, time: Time.now, limit: SunbusConfig::MAX_DEPARTURES) stop = find_stop(query, region: region, fetch: fetch) return [] unless stop departures_at_stop(stop, region: region, fetch: fetch, time: time, limit: limit) end
Source
# File lib/sunbus/gtfs.rb, line 241 def parse_gtfs_clock(value) parts = value.to_s.split(":").map(&:to_i) (parts[0] * 3600) + (parts[1] * 60) + parts[2] end
Source
# File lib/sunbus/gtfs.rb, line 85 def parse_zip(path) return empty_feed unless File.file?(path) tables = {} Zip::File.open(path) do |zip| %w[stops routes trips stop_times calendar calendar_dates].each do |name| entry = zip.find_entry("#{name}.txt") tables[name] = entry ? CSV.parse(entry.get_input_stream.read, headers: true) : [] end end build_feed(tables) end
Source
# File lib/sunbus/gtfs.rb, line 330 def plan_trip(from:, to:, region: SunbusConfig::DEFAULT_REGION, fetch: nil, time: Time.now) require_relative "connect" SunbusConnect.plan(from: from, to: to, region: region, fetch: fetch, time: time) end
Source
# File lib/sunbus/gtfs.rb, line 298 def realtime_departures(stop_id, region:, fetch:, time:) meta = SunbusConfig.region_meta(region) rt_region = meta[:rt_region] return [] unless rt_region message = QldTranslinkRt.fetch_feed(region: rt_region, feed: QldTranslinkRt::FEED_TRIP_UPDATES, fetch: fetch) QldTranslinkRt.departures_for_stops([stop_id], message: message, time: time, limit: SunbusConfig::MAX_DEPARTURES) .map do |row| at = row[:time].getlocal("+10:00") depart_sec = (at.hour * 3600) + (at.min * 60) + at.sec { route_short_name: row[:route_id].to_s, route_long_name: "", headsign: "live", departure_time: at.strftime("%H:%M"), depart_sec: depart_sec, stop_id: stop_id, live: true, delay_sec: row[:delay_sec] } end rescue StandardError [] end
Source
# File lib/sunbus/gtfs.rb, line 73 def refresh_zip!(path, region:, fetch: nil, time: Time.now) url = SunbusConfig.gtfs_zip_url(region) FileUtils.mkdir_p(File.dirname(path)) body = if fetch fetch.call(url) else RabbotHttp.fetch(url, user_agent: "Rabbot/1.0 (sunbus gtfs)") end File.binwrite(path, body) File.utime(time, time, path) end
Source
# File lib/sunbus/gtfs.rb, line 185 def search_stops(query, region: SunbusConfig::DEFAULT_REGION, fetch: nil, limit: SunbusConfig::MAX_STOPS) feed_data = feed(region: region, fetch: fetch) stops = feed_data[:stops] matches = SunbusStopSearch.search(stops, query, region: region, limit: limit) return matches unless matches.empty? SunbusRssRoutes.stops_for_query(query, feed: feed_data, region: region, fetch: fetch, limit: limit) end
Source
# File lib/sunbus/gtfs.rb, line 216 def service_active?(service_id, date, feed) ymd = date.strftime("%Y%m%d") exceptions = feed[:calendar_dates][service_id] removed = exceptions.any? { |row| row[:date] == ymd && row[:exception_type] == 2 } added = exceptions.any? { |row| row[:date] == ymd && row[:exception_type] == 1 } return true if added return false if removed cal = feed[:calendar][service_id] return false unless cal return false if ymd < cal[:start_date] || ymd > cal[:end_date] case date.wday when 0 then cal[:sunday] when 1 then cal[:monday] when 2 then cal[:tuesday] when 3 then cal[:wednesday] when 4 then cal[:thursday] when 5 then cal[:friday] when 6 then cal[:saturday] else false end end
Source
# File lib/sunbus/gtfs.rb, line 198 def stops_for_suburb(query, region: SunbusConfig::DEFAULT_REGION, fetch: nil, limit: SunbusConfig::MAX_SUBURB_STOPS) feed_data = feed(region: region, fetch: fetch) suburb_stops(query, feed_data[:stops], region: region, limit: limit) end
Source
# File lib/sunbus/gtfs.rb, line 204 def suburb_stops(query, stops, region: SunbusConfig::DEFAULT_REGION, limit: SunbusConfig::MAX_SUBURB_STOPS, suburb_entry: nil) suburb = suburb_entry || SunbusSuburbs.resolve(query, region: region) return { error: "Unknown suburb — try !sunbus stop #{query}" } unless suburb geo_ranked = SunbusStopSearch.rank_by_suburb(stops, suburb).first(limit) return { stops: geo_ranked, suburb: query } unless geo_ranked.empty? name_ranked = SunbusStopSearch.search(stops, query, region: region, limit: limit) { stops: name_ranked, suburb: query } end
Source
# File lib/sunbus/gtfs.rb, line 69 def zip_fresh?(path, time: Time.now) File.file?(path) && (time - File.mtime(path)) < SunbusConfig::GTFS_CACHE_MAX_AGE_SEC end