octranspo_fetch 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []