class OCTranspo

Constants

BASE_URL
NEXT_TRIPS_CACHE_SIZE
OCT_NS
ROUTE_CACHE_SIZE

Public Class Methods

new(options) click to toggle source

Create a new OCTranspo

Arguments:

options[:application_id]: (String) Your application ID, assigned by OC Transpo.
options[:application_key]: (String) Your application key, assigned by OC Transpo.
# File lib/octranspo_fetch.rb, line 15
def initialize(options)
    @app_id = options[:application_id]
    @app_key = options[:application_key]
    @next_trips_cache = LruRedux::Cache.new(NEXT_TRIPS_CACHE_SIZE)
    @route_summary_cache = LruRedux::Cache.new(ROUTE_CACHE_SIZE)
    @api_calls = 0
    @cache_hits = 0
    @cache_misses = 0
end

Public Instance Methods

cache_stats() click to toggle source
# File lib/octranspo_fetch.rb, line 35
def cache_stats()
    return {hits: @cache_hits, misses: @cache_misses}
end
clear_cache() click to toggle source
# File lib/octranspo_fetch.rb, line 25
def clear_cache()
    @next_trips_cache.clear()
    @route_summary_cache.clear()
end
get_next_trips_for_stop(stop, route_no=nil, options={}) click to toggle source

Get the next three trips for the given stop. Note this may return data for the same route number in multiple headings.

Arguments:

stop: (String) The stop number.
route_no: (String) The route number, optional. If nil (default), get all routes.
options[:max_cache_time]: (Integer) Maximum cache age, in seconds.  If cached data is
  available and is newer than this, then the cached value will be returned.  Defaults
  to five minutes.
# File lib/octranspo_fetch.rb, line 98
def get_next_trips_for_stop(stop, route_no=nil, options={})
    max_cache_time = (options[:max_cache_time] or 60*5)

    # Return result from cache, if available
    cache_key = "#{stop}-#{route_no.to_s}"
    cached_result = @next_trips_cache[cache_key]
    if !cached_result.nil? and ((cached_result[:time] + max_cache_time) > Time.now.to_i)
        @cache_hits += 1
        return adjust_cached_trip_times(cached_result[:next_trips])
    end
    @cache_misses += 1
    if route_no.nil?
      parts = {
        :method => "GetNextTripsForStopAllRoutes",
        :args => "stopNo=#{stop}",
        :alt_resource => "GetRouteSummaryForStop",
        :stop_description => "t:StopDescription",
        :route_xpath => 't:Routes/t:Route',
        :route_error => "Error for route",
        :route_label => "t:RouteHeading"
      }
    else
      parts = {
        :method => "GetNextTripsForStop",
        :args => "stopNo=#{stop}&routeNo=#{route_no}",
        :alt_resource => nil,
        :stop_description => "t:StopLabel",
        :route_xpath => 't:Route/t:RouteDirection',
        :route_error => "Error for route: #{route_no}",
        :route_label => "t:RouteLabel"
      }
    end

    xresult = fetch parts[:method], parts[:args], parts[:alt_resource]

    result = {
        stop: get_value(xresult, "t:StopNo"),
        stop_description: get_value(xresult, parts[:stop_description]),
        time: Time.now,
        routes: []
    }

    found_data = false

    xresult.xpath(parts[:route_xpath], OCT_NS).each do |route|
        get_error(route, parts[:route_error])

        route_obj = {
            cached: false,
            route: get_value(route, "t:RouteNo"),
            route_label: get_value(route, parts[:route_label]),
            direction: get_value(route, "t:Direction"),
            request_processing_time: route_no.nil? ? nil : Time.parse(get_value(route, "t:RequestProcessingTime")),
            trips: []
        }
        route.xpath('t:Trips/t:Trip', OCT_NS).each do |trip|
            route_obj[:trips].push({
                destination: get_value(trip, "t:TripDestination"), # e.g. "Barhaven"
                start_time: get_value(trip, "t:TripStartTime"), # e.g. "14:25" TODO: parse to time
                adjusted_schedule_time: get_value(trip, "t:AdjustedScheduleTime").to_i, # Adjusted schedule time in minutes
                adjustment_age: get_value(trip, "t:AdjustmentAge").to_f, # Time since schedule was adjusted in minutes
                last_trip: (get_value(trip, "t:LastTripOfSchedule") == "true"),
                bus_type: get_value(trip, "t:BusType"),
                gps_speed: get_value(trip, "t:GPSSpeed").to_f,
                latitude: get_value(trip, "t:Latitude").to_f,
                longitude: get_value(trip, "t:Longitude").to_f
            })
        end

        if route_obj[:trips].length != 0
            # Assume that if any trips are filled in, then all the trips will be filled in?
            # Is this a safe assumption?
            found_data = true
        end

        result[:routes].push route_obj
    end

    # Sometimes OC Transpo doesn't return any data for a route, even though it should.  When
    # this happens, if we have cached data, we use that, even if it's slightly stale.
    if !found_data and !cached_result.nil? and (get_trip_count(cached_result[:next_trips]) > 0)
        # Use the cached data, even if it's stale
        result = adjust_cached_trip_times(cached_result[:next_trips])
    else
        @next_trips_cache[cache_key] = {
            next_trips: result,
            time: Time.now.to_i
        }
    end


    return result
end
get_route_summary_for_stop(stop, options={}) click to toggle source

Get a list of routes for a specific stop.

Returns a {stop, stop_description, routes: [{route, direction_id, direction, heading}]} object. Note that route data is cached.

Arguments:

stop: (String) The stop number.
options[:max_cache_time]: (Integer) Maximum cache age, in seconds.  If cached data is
  available and is newer than this, then the cached value will be returned.  Defaults to
  one day.
# File lib/octranspo_fetch.rb, line 50
def get_route_summary_for_stop(stop, options={})
    max_cache_time = (options[:max_cache_time] or 60*60*24)
    cached_result = @route_summary_cache[stop]
    if !cached_result.nil? and ((cached_result[:time] + max_cache_time) > Time.now.to_i)
        @cache_hits += 1
        return cached_result[:route_summary]
    end
    @cache_misses += 1

    xresult = fetch "GetRouteSummaryForStop", "stopNo=#{stop}"

    result = {
        stop: get_value(xresult, "t:StopNo"),
        stop_description: get_value(xresult, "t:StopDescription"),
        routes: []
    }

    xresult.xpath('t:Routes/t:Route', OCT_NS).each do |route|
        result[:routes].push({
            route: get_value(route, "t:RouteNo"),
            direction_id: get_value(route, "t:DirectionID"),
            direction: get_value(route, "t:Direction"),
            heading: get_value(route, "t:RouteHeading")
        })
    end

    if result[:routes].length == 0
        raise "No routes found"
    end

    @route_summary_cache[stop] = {
        route_summary: result,
        time: Time.now.to_i
    }

    return result
end
requests() click to toggle source

Returns the number of API calls made by this instance since it was created.

# File lib/octranspo_fetch.rb, line 31
def requests()
    return @api_calls
end
simple_get_next_trips_for_stop(stop, route_nos=nil, route_label=nil) click to toggle source

Returns an array of `{stop, stop_description, route_no, route_label, direction, arrival_in_minutes, …}` objects.

`…` is any data that would be available from a `trip` object from `get_next_trips_for_stop()` (e.g. gps_speed, latitude, longitude, etc…) Returned results are sorted in ascending arrival time.

Arguments:

stop: (String) The stop number.
route_nos: ([String]) can be a single route number, or an array of route numbers, or nil.
    If nil, then this method will call get_route_summary_for_stop to get a list of routes.
route_label: (String) If "route_label" is supplied, then only trips with a matching
    route_label will be returned.
# File lib/octranspo_fetch.rb, line 206
def simple_get_next_trips_for_stop(stop, route_nos=nil, route_label=nil)
    answer = []
    if route_nos.nil?
        route_summary = get_route_summary_for_stop(stop)
        route_nos = route_summary[:routes].map { |e| e[:route] }

    elsif !route_nos.kind_of?(Array)
        route_nos = [route_nos] end

    route_nos.uniq.each do |route_no|
        oct_result = get_next_trips_for_stop stop, route_no
        oct_result[:routes].each do |route|
            if route_label.nil? or (route[:route_label] == route_label)
                route[:trips].each do |trip|
                    answer.push(trip.merge({
                        stop: oct_result[:stop],
                        stop_description: oct_result[:stop_description],
                        route_no: route[:route],
                        route_label: route[:route_label],
                        direction: route[:direction],
                        arrival_in_minutes: trip[:adjusted_schedule_time],
                        live: (trip[:adjustment_age] > 0)
                    }))
                end
            end
        end
    end

    answer.sort! { |a,b| a[:arrival_in_minutes] <=> b[:arrival_in_minutes] }

    return answer
end

Private Instance Methods

adjust_cached_trip_times(cached_routes) click to toggle source

When returning cached trips, we need to adjust the `:adjustment_age` and `:adjusted_schedule_time` of each entry to reflect how long the object has been sitting in the cache.

# File lib/octranspo_fetch.rb, line 320
def adjust_cached_trip_times(cached_routes)

    cached_routes = deep_copy cached_routes
    cached_routes[:cached] = true

    time_delta = Time.now.to_i - cached_routes[:time].to_i
    cached_routes[:routes].each do |route_obj|
        route_obj[:trips].each do |trip|
            trip[:adjusted_schedule_time] -= (time_delta.to_f / 60).round
            if trip[:adjustment_age] > 0
                trip[:adjustment_age] += time_delta.to_f / 60
            end
        end

        # Filter out results with negative arrival times, since they've probably
        # already gone by.
        route_obj[:trips].select! { |trip| trip[:adjusted_schedule_time] >= 0 }
    end

    return cached_routes
end
deep_copy(o) click to toggle source
# File lib/octranspo_fetch.rb, line 313
def deep_copy(o)
    Marshal.load(Marshal.dump(o))
end
fetch(resource, params, alt_resource = nil) click to toggle source

Fetch and parse some data from the OC-Transpo API. Returns a nokogiri object for the Result within the XML document.

# File lib/octranspo_fetch.rb, line 248
def fetch(resource, params, alt_resource = nil)
    @api_calls = (@api_calls + 1)
    response = RestClient.post("#{BASE_URL}/#{resource}",
        "appID=#{@app_id}&apiKey=#{@app_key}&#{params}")
    if alt_resource.nil?
      alt_resource = resource
    end
    doc = Nokogiri::XML(response.body)
    xresult = doc.xpath("//oct:#{alt_resource}Result", OCT_NS)
    if xresult.length == 0
        raise "Error: No reply for #{resource}"
    end

    get_error(xresult, "Error for #{params}:")

    return xresult
end
get_child(node, el) click to toggle source

Return a single child from a nokogiri document.

# File lib/octranspo_fetch.rb, line 279
def get_child(node, el)
    return node.at_xpath(el, OCT_NS)
end
get_error(node, message="") click to toggle source

Fetch an OC-Transpo “Error” from a node.

# File lib/octranspo_fetch.rb, line 291
def get_error(node, message="")
    xerror = get_child(node, "t:Error")
    if (!xerror.nil? and !xerror.content.empty?)
        error = xerror.content
        error = case error
            when "1"
                "Invalid API key"
            when "2"
                "Unable to query data source"
            when "10"
                "Invalid stop number"
            when "11"
                "Invalid route number"
            when "12"
                "Stop does not service route"
            else
                error
        end
        raise "#{message}: #{error}"
    end
end
get_trip_count(routes) click to toggle source

Count the number of trips in a result from OC Transpo

# File lib/octranspo_fetch.rb, line 267
def get_trip_count(routes)
    answer = 0
    if !routes.nil?
        routes[:routes].each do |route|
            answer += route[:trips].length
        end
    end

    return answer
end
get_value(node, el) click to toggle source

Return the value of a child from a nokogiri document.

# File lib/octranspo_fetch.rb, line 284
def get_value(node, el)
    child = node.at_xpath(el, OCT_NS)
    if child.nil? then raise "Could not find child element #{el}" end
    return child.content
end