octranspo_fetch 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. data/lib/octranspo_fetch.rb +258 -0
  2. metadata +94 -0
@@ -0,0 +1,258 @@
1
+ # OC Transpo
2
+
3
+ require "rest-client"
4
+ require 'nokogiri'
5
+ require 'time'
6
+ require 'lru_redux'
7
+
8
+ class OCTranspo
9
+ # Create a new OCTranspo
10
+ #
11
+ # Arguments:
12
+ # options[:application_id]: (String) Your application ID, assigned by OC Transpo.
13
+ # options[:application_key]: (String) Your application key, assigned by OC Transpo.
14
+ #
15
+ def initialize(options)
16
+ @app_id = options[:application_id]
17
+ @app_key = options[:application_key]
18
+ @trips_cache = LruRedux::Cache.new(TRIPS_CACHE_SIZE)
19
+ @route_summary_cache = LruRedux::Cache.new(ROUTE_CACHE_SIZE)
20
+ @api_calls = 0
21
+ end
22
+
23
+ def clear_cache()
24
+ @trips_cache.clear()
25
+ @route_summary_cache.clear()
26
+ end
27
+
28
+ # Returns the number of API calls made by this instance since it was created.
29
+ def requests()
30
+ return @api_calls
31
+ end
32
+
33
+ # Get a list of routes for a specific stop.
34
+ #
35
+ # Returns a {stop, stop_description, routes: [{route, direction_id, direction, heading}]} object.
36
+ # Note that route data is cached.
37
+ #
38
+ # Arguments:
39
+ # stop: (String) The stop number.
40
+ #
41
+ def get_route_summary_for_stop(stop)
42
+ cached_result = @route_summary_cache[stop]
43
+ if !cached_result.nil? then return cached_result end
44
+
45
+ xresult = fetch "GetRouteSummaryForStop", "stopNo=#{stop}"
46
+
47
+ result = {
48
+ stop: get_value(xresult, "t:StopNo"),
49
+ stop_description: get_value(xresult, "t:StopDescription"),
50
+ routes: []
51
+ }
52
+
53
+ xresult.xpath('t:Routes/t:Route', OCT_NS).each do |route|
54
+ result[:routes].push({
55
+ route: get_value(route, "t:RouteNo"),
56
+ direction_id: get_value(route, "t:DirectionID"),
57
+ direction: get_value(route, "t:Direction"),
58
+ heading: get_value(route, "t:RouteHeading")
59
+ })
60
+ end
61
+
62
+ if result[:routes].length == 0
63
+ raise "No routes found"
64
+ end
65
+
66
+ @route_summary_cache[stop] = result
67
+
68
+ return result
69
+ end
70
+
71
+ # Get the next three trips for the given stop. Note this may return data for the same route
72
+ # number in multiple headings.
73
+ #
74
+ # Arguments:
75
+ # stop: (String) The stop number.
76
+ # route_no: (String) The route number.
77
+ #
78
+ def get_next_trips_for_stop(stop, route_no)
79
+ xresult = fetch "GetNextTripsForStop", "stopNo=#{stop}&routeNo=#{route_no}"
80
+
81
+ result = {
82
+ stop: get_value(xresult, "t:StopNo"),
83
+ stop_description: get_value(xresult, "t:StopLabel"),
84
+ routes: []
85
+ }
86
+
87
+ xresult.xpath('t:Route/t:RouteDirection', OCT_NS).each do |route|
88
+ get_error(route, "Error for route: #{route_no}")
89
+
90
+ route_obj = {
91
+ cached: false,
92
+ route: get_value(route, "t:RouteNo"),
93
+ route_label: get_value(route, "t:RouteLabel"),
94
+ direction: get_value(route, "t:Direction"),
95
+ request_processing_time: Time.parse(get_value(route, "t:RequestProcessingTime")),
96
+ trips: []
97
+ }
98
+ route.xpath('t:Trips/t:Trip', OCT_NS).each do |trip|
99
+ route_obj[:trips].push({
100
+ destination: get_value(trip, "t:TripDestination"), # e.g. "Barhaven"
101
+ start_time: get_value(trip, "t:TripStartTime"), # e.g. "14:25" TODO: parse to time
102
+ adjusted_schedule_time: get_value(trip, "t:AdjustedScheduleTime").to_i, # Adjusted schedule time in minutes
103
+ adjustment_age: get_value(trip, "t:AdjustmentAge").to_f, # Time since schedule was adjusted in minutes
104
+ last_trip: (get_value(trip, "t:LastTripOfSchedule") == "true"),
105
+ bus_type: get_value(trip, "t:BusType"),
106
+ gps_speed: get_value(trip, "t:GPSSpeed").to_f,
107
+ latitude: get_value(trip, "t:Latitude").to_f,
108
+ longitude: get_value(trip, "t:Longitude").to_f
109
+ })
110
+ end
111
+
112
+ cache_key = "#{stop}-#{route_obj[:route]}-#{route_obj[:direction]}"
113
+ if route_obj[:trips].length == 0
114
+ # Sometimes OC Transpo doesn't return any data. When this happens, fetch data from the cache.
115
+ trips = @trips_cache[cache_key]
116
+ if !trips.nil?
117
+ time_delta = Time.now.to_i - trips[:time]
118
+ route_obj[:request_processing_time] += time_delta
119
+ route_obj[:trips] = deep_copy(trips[:trips])
120
+ route_obj[:trips].each do |trip|
121
+ route_obj[:cached] = true
122
+ trip[:adjusted_schedule_time] += (time_delta.to_f / 60).round
123
+ if trip[:adjustment_age] > 0
124
+ trip[:adjustment_age] += time_delta.to_f / 60
125
+ end
126
+ end
127
+
128
+ else
129
+ # No data in the cache... Hrm...
130
+ end
131
+
132
+ else
133
+ # Cache the trips for later
134
+ @trips_cache[cache_key] = {
135
+ time: Time.now.to_i,
136
+ trips: route_obj[:trips]
137
+ }
138
+ end
139
+
140
+ result[:routes].push route_obj
141
+ end
142
+
143
+
144
+ return result
145
+ end
146
+
147
+ # Returns an array of
148
+ # `{stop, stop_description, route_no, route_label, direction, arrival_in_minutes, ...}` objects.
149
+ #
150
+ # `...` is any data that would be available from a `trip` object from
151
+ # `get_next_trips_for_stop()` (e.g. gps_speed, latitude, longitude, etc...)
152
+ #
153
+ # Arguments:
154
+ # stop: (String) The stop number.
155
+ # route_nos: ([String]) can be a single route number, or an array of route numbers, or nil.
156
+ # If nil, then this method will call get_route_summary_for_stop to get a list of routes.
157
+ # route_label: (String) If "route_label" is supplied, then only trips with a matching
158
+ # route_label will be returned.
159
+ #
160
+ def simple_get_next_trips_for_stop(stop, route_nos=nil, route_label=nil)
161
+ answer = []
162
+ if route_nos.nil?
163
+ route_summary = get_route_summary_for_stop(stop)
164
+ route_nos = route_summary[:routes].map { |e| e[:route] }
165
+
166
+ elsif !route_nos.kind_of?(Array)
167
+ route_nos = [route_nos] end
168
+
169
+ route_nos.uniq.each do |route_no|
170
+ oct_result = get_next_trips_for_stop stop, route_no
171
+ oct_result[:routes].each do |route|
172
+ if route_label.nil? or (route[:route_label] == route_label)
173
+ route[:trips].each do |trip|
174
+ answer.push(trip.merge({
175
+ stop: oct_result[:stop],
176
+ stop_description: oct_result[:stop_description],
177
+ route_no: route[:route],
178
+ route_label: route[:route_label],
179
+ direction: route[:direction],
180
+ arrival_in_minutes: trip[:adjusted_schedule_time],
181
+ live: (trip[:adjustment_age] > 0)
182
+ }))
183
+ end
184
+ end
185
+ end
186
+ end
187
+
188
+ answer.sort! { |a,b| a[:arrival_in_minutes] <=> b[:arrival_in_minutes] }
189
+
190
+ return answer
191
+ end
192
+
193
+ private
194
+
195
+ BASE_URL = "https://api.octranspo1.com/v1.1"
196
+ OCT_NS = {'oct' => 'http://octranspo.com', 't' => 'http://tempuri.org/'}
197
+ TRIPS_CACHE_SIZE = 100
198
+ ROUTE_CACHE_SIZE = 100
199
+
200
+ # Fetch and parse some data from the OC-Transpo API. Returns a nokogiri object for
201
+ # the Result within the XML document.
202
+ def fetch(resource, params)
203
+ @api_calls = (@api_calls + 1)
204
+
205
+ response = RestClient.post("#{BASE_URL}/#{resource}",
206
+ "appID=#{@app_id}&apiKey=#{@app_key}&#{params}")
207
+
208
+ doc = Nokogiri::XML(response.body)
209
+ xresult = doc.xpath("//oct:#{resource}Result", OCT_NS)
210
+ if xresult.length == 0
211
+ raise "Error: No reply for #{resource}"
212
+ end
213
+
214
+ get_error(xresult, "Error for #{params}:")
215
+
216
+ return xresult
217
+ end
218
+
219
+ # Return a single child from a nokogiri document.
220
+ def get_child(node, el)
221
+ return node.at_xpath(el, OCT_NS)
222
+ end
223
+
224
+ # Return the value of a child from a nokogiri document.
225
+ def get_value(node, el)
226
+ child = node.at_xpath(el, OCT_NS)
227
+ if child.nil? then raise "Could not find child element #{el}" end
228
+ return child.content
229
+ end
230
+
231
+ # Fetch an OC-Transpo "Error" from a node.
232
+ def get_error(node, message="")
233
+ xerror = get_child(node, "t:Error")
234
+ if (!xerror.nil? and !xerror.content.empty?)
235
+ error = xerror.content
236
+ error = case error
237
+ when "1"
238
+ "Invalid API key"
239
+ when "2"
240
+ "Unable to query data source"
241
+ when "10"
242
+ "Invalid stop number"
243
+ when "11"
244
+ "Invalid route number"
245
+ when "12"
246
+ "Stop does not service route"
247
+ else
248
+ error
249
+ end
250
+ raise "#{message}: #{error}"
251
+ end
252
+ end
253
+
254
+ def deep_copy(o)
255
+ Marshal.load(Marshal.dump(o))
256
+ end
257
+ end
258
+
metadata ADDED
@@ -0,0 +1,94 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: octranspo_fetch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jason Walton
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-09-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nokogiri
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: 1.5.10
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 1.5.10
30
+ - !ruby/object:Gem::Dependency
31
+ name: rest-client
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 1.6.7
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 1.6.7
46
+ - !ruby/object:Gem::Dependency
47
+ name: lru_redux
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: 0.8.1
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: 0.8.1
62
+ description: A simple wrapper around the OC Transpo API with some minimal caching.
63
+ email:
64
+ executables: []
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - lib/octranspo_fetch.rb
69
+ homepage: http://rubygems.org/gems/octranspo_fetch
70
+ licenses:
71
+ - MIT
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 1.8.23
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: Fetch data from OC Tranpo API
94
+ test_files: []