cta_redux 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +37 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +16 -0
- data/Rakefile +2 -0
- data/cta_redux.gemspec +32 -0
- data/data/.gitkeep +0 -0
- data/data/cta-gtfs.db.gz +0 -0
- data/lib/cta_redux/api/api_response.rb +45 -0
- data/lib/cta_redux/api/bus_tracker.rb +178 -0
- data/lib/cta_redux/api/customer_alerts.rb +68 -0
- data/lib/cta_redux/api/train_tracker.rb +89 -0
- data/lib/cta_redux/bus_tracker.rb +183 -0
- data/lib/cta_redux/customer_alerts.rb +72 -0
- data/lib/cta_redux/faraday_middleware/bus_tracker_parser.rb +46 -0
- data/lib/cta_redux/faraday_middleware/customer_alerts_parser.rb +39 -0
- data/lib/cta_redux/faraday_middleware/simple_cache.rb +32 -0
- data/lib/cta_redux/faraday_middleware/train_tracker_parser.rb +37 -0
- data/lib/cta_redux/models/agency.rb +4 -0
- data/lib/cta_redux/models/bus.rb +46 -0
- data/lib/cta_redux/models/calendar.rb +7 -0
- data/lib/cta_redux/models/route.rb +62 -0
- data/lib/cta_redux/models/shape.rb +4 -0
- data/lib/cta_redux/models/stop.rb +66 -0
- data/lib/cta_redux/models/stop_time.rb +6 -0
- data/lib/cta_redux/models/train.rb +74 -0
- data/lib/cta_redux/models/transfer.rb +6 -0
- data/lib/cta_redux/models/trip.rb +64 -0
- data/lib/cta_redux/train_tracker.rb +103 -0
- data/lib/cta_redux/version.rb +3 -0
- data/lib/cta_redux.rb +50 -0
- data/script/gtfs_to_sqlite.rb +137 -0
- data/spec/bus_tracker_spec.rb +149 -0
- data/spec/customer_alerts_spec.rb +48 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/stubs/alerts_response.xml +362 -0
- data/spec/stubs/getdirections_response.xml +16 -0
- data/spec/stubs/getpatterns_rt22_response.xml +2768 -0
- data/spec/stubs/getpredictions_rt22stpid15895_response.xml +59 -0
- data/spec/stubs/getpredictions_vid4361_response.xml +647 -0
- data/spec/stubs/getroutes_response.xml +774 -0
- data/spec/stubs/getservicebulletins_rt8_response.xml +110 -0
- data/spec/stubs/getstops_response.xml +614 -0
- data/spec/stubs/gettime_response.xml +2 -0
- data/spec/stubs/getvehicles_rt22_response.xml +239 -0
- data/spec/stubs/getvehicles_vid4394_response.xml +23 -0
- data/spec/stubs/route_status8_response.xml +1 -0
- data/spec/stubs/routes_response.xml +1 -0
- data/spec/stubs/ttarivals_stpid30141_response.xml +1 -0
- data/spec/stubs/ttfollow_run217_response.xml +1 -0
- data/spec/stubs/ttpositions_response.xml +1 -0
- data/spec/train_tracker_spec.rb +70 -0
- metadata +234 -0
@@ -0,0 +1,183 @@
|
|
1
|
+
module CTA
|
2
|
+
class BusTracker
|
3
|
+
def self.connection
|
4
|
+
raise "You need to set a developer key first. Try CTA::BusTracker.key = 'foo'." unless @key
|
5
|
+
|
6
|
+
@connection ||= Faraday.new do |faraday|
|
7
|
+
faraday.url_prefix = 'http://www.ctabustracker.com/bustime/api/v2/'
|
8
|
+
faraday.params = { :key => @key }
|
9
|
+
|
10
|
+
faraday.use CTA::BusTracker::Parser, !!@debug
|
11
|
+
faraday.response :caching, SimpleCache.new(Hash.new)
|
12
|
+
faraday.adapter Faraday.default_adapter
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.time!
|
17
|
+
connection.get('gettime')
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.vehicles!(options={})
|
21
|
+
allowed_keys = [:vehicles, :routes]
|
22
|
+
if options.keys.any? { |k| !allowed_keys.include?(k) }
|
23
|
+
raise "Illegal option!"
|
24
|
+
end
|
25
|
+
|
26
|
+
has_vehicle = options.has_key?(:vehicles)
|
27
|
+
has_route = options.has_key?(:routes)
|
28
|
+
|
29
|
+
if !(has_vehicle || has_route) || (has_vehicle && has_route)
|
30
|
+
raise "Must specify either vehicle OR route options! Try vehicles(:routes => 37)"
|
31
|
+
end
|
32
|
+
|
33
|
+
vehicles = Array.wrap(options[:vehicles]).flatten.compact.uniq.join(',')
|
34
|
+
routes = Array.wrap(options[:routes]).flatten.compact.uniq.join(',')
|
35
|
+
|
36
|
+
connection.get('getvehicles', { :rt => routes, :vid => vehicles })
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.routes!
|
40
|
+
connection.get('getroutes')
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.directions!(options={})
|
44
|
+
allowed_keys = [:route]
|
45
|
+
if options.keys.any? { |k| !allowed_keys.include?(k) }
|
46
|
+
raise "Illegal option!"
|
47
|
+
end
|
48
|
+
|
49
|
+
unless options.has_key?(:route)
|
50
|
+
raise "Must specify a route! (Try directions(:route => 914) )"
|
51
|
+
end
|
52
|
+
|
53
|
+
routes = Array.wrap(options[:route]).flatten.compact.uniq
|
54
|
+
|
55
|
+
if routes.size > 1
|
56
|
+
raise "Only one route may be specified!"
|
57
|
+
end
|
58
|
+
connection.get('getdirections', { :rt => routes.first })
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.stops!(options={})
|
62
|
+
allowed_keys = [:route, :direction]
|
63
|
+
if options.keys.any? { |k| !allowed_keys.include?(k) }
|
64
|
+
raise "Illegal option!"
|
65
|
+
end
|
66
|
+
|
67
|
+
has_route = options.has_key?(:route)
|
68
|
+
has_direction = options.has_key?(:direction)
|
69
|
+
|
70
|
+
if !(has_direction && has_route)
|
71
|
+
raise "Must specify both direction and route options! Try stops(:route => 37, :direction => :northbound)"
|
72
|
+
end
|
73
|
+
|
74
|
+
routes = Array.wrap(options[:route]).flatten.compact.uniq
|
75
|
+
directions = Array.wrap(options[:direction]).flatten.compact.uniq
|
76
|
+
if routes.size > 1
|
77
|
+
raise "Only one route may be specified!"
|
78
|
+
end
|
79
|
+
|
80
|
+
if directions.size > 1
|
81
|
+
raise "Only one direction may be specified!"
|
82
|
+
end
|
83
|
+
|
84
|
+
connection.get('getstops', { :rt => routes.first, :dir => directions.first.to_s.capitalize })
|
85
|
+
end
|
86
|
+
|
87
|
+
def self.patterns!(options={})
|
88
|
+
allowed_keys = [:route, :patterns]
|
89
|
+
if options.keys.any? { |k| !allowed_keys.include?(k) }
|
90
|
+
raise "Illegal option!"
|
91
|
+
end
|
92
|
+
|
93
|
+
has_route = options.has_key?(:route)
|
94
|
+
has_pattern = options.has_key?(:patterns)
|
95
|
+
|
96
|
+
if !(has_pattern || has_route) || (has_pattern && has_route)
|
97
|
+
raise "Must specify a pattern OR route option! Try patterns(:route => 37)"
|
98
|
+
end
|
99
|
+
|
100
|
+
routes = Array.wrap(options[:route]).flatten.compact.uniq
|
101
|
+
patterns = Array.wrap(options[:patterns]).flatten.compact.uniq.join(',')
|
102
|
+
if routes.size > 1
|
103
|
+
raise "Only one route may be specified!"
|
104
|
+
end
|
105
|
+
|
106
|
+
connection.get('getpatterns', { :pid => patterns, :rt => routes.first })
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.predictions!(options={})
|
110
|
+
allowed_keys = [:vehicles, :stops, :routes, :limit]
|
111
|
+
if options.keys.any? { |k| !allowed_keys.include?(k) }
|
112
|
+
raise "Illegal option!"
|
113
|
+
end
|
114
|
+
|
115
|
+
has_vehicle = options.has_key?(:vehicles)
|
116
|
+
has_stop = options.has_key?(:stops)
|
117
|
+
|
118
|
+
if !(has_stop || has_vehicle) || (has_stop && has_vehicle)
|
119
|
+
raise "Must specify a stop (and optionally route), or vehicles! Try predictions(:stops => 6597)"
|
120
|
+
end
|
121
|
+
|
122
|
+
routes = Array.wrap(options[:routes]).flatten.compact.uniq.join(',')
|
123
|
+
stops = Array.wrap(options[:stops]).flatten.compact.uniq.join(',')
|
124
|
+
vehicles = Array.wrap(options[:vehicles]).flatten.compact.uniq.join(',')
|
125
|
+
limit = Array.wrap(options[:limit]).first.to_i if options.has_key?(:limit)
|
126
|
+
|
127
|
+
connection.get('getpredictions', { :rt => routes, :vid => vehicles, :stpid => stops, :top => limit })
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.bulletins!(options={})
|
131
|
+
allowed_keys = [:routes, :directions, :stop]
|
132
|
+
if options.keys.any? { |k| !allowed_keys.include?(k) }
|
133
|
+
raise "Illegal option!"
|
134
|
+
end
|
135
|
+
|
136
|
+
has_route = options.has_key?(:routes)
|
137
|
+
has_stop = options.has_key?(:stop)
|
138
|
+
|
139
|
+
if !(has_route || has_stop)
|
140
|
+
raise "Must provide at least a route or a stop! Try bulletins(:routes => 22)"
|
141
|
+
end
|
142
|
+
|
143
|
+
directions = Array.wrap(options[:direction]).flatten.compact.uniq
|
144
|
+
routes = Array.wrap(options[:routes]).flatten.compact.uniq
|
145
|
+
stops = Array.wrap(options[:stop]).flatten.compact.uniq
|
146
|
+
|
147
|
+
if directions.size > 1
|
148
|
+
raise "Only one direction may be specified!"
|
149
|
+
end
|
150
|
+
|
151
|
+
if directions.any? && routes.size != 1
|
152
|
+
raise "Must specify one and only one route when combined with a direction"
|
153
|
+
end
|
154
|
+
|
155
|
+
if (directions.any? || routes.any?) && stops.size > 1
|
156
|
+
raise "Cannot specify more than one stop when combined with a route and direction"
|
157
|
+
end
|
158
|
+
|
159
|
+
routes = routes.join(',')
|
160
|
+
stops = stops.join(',')
|
161
|
+
|
162
|
+
connection.get('getservicebulletins', { :rt => routes, :stpid => stops, :dir => directions.first })
|
163
|
+
end
|
164
|
+
|
165
|
+
def self.key
|
166
|
+
@key
|
167
|
+
end
|
168
|
+
|
169
|
+
def self.key=(key)
|
170
|
+
@key = key
|
171
|
+
@connection = nil
|
172
|
+
end
|
173
|
+
|
174
|
+
def self.debug
|
175
|
+
!!@debug
|
176
|
+
end
|
177
|
+
|
178
|
+
def self.debug=(debug)
|
179
|
+
@debug = debug
|
180
|
+
@connection = nil
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module CTA
|
2
|
+
class CustomerAlerts
|
3
|
+
|
4
|
+
def self.connection
|
5
|
+
@connection ||= Faraday.new do |faraday|
|
6
|
+
faraday.url_prefix = 'http://www.transitchicago.com/api/1.0/'
|
7
|
+
faraday.use CTA::CustomerAlerts::Parser, !!@debug
|
8
|
+
faraday.response :caching, SimpleCache.new(Hash.new)
|
9
|
+
faraday.adapter Faraday.default_adapter
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.status!(options = {})
|
14
|
+
allowed_keys = [:routes, :stations]
|
15
|
+
if options.keys.any? { |k| !allowed_keys.include?(k) }
|
16
|
+
raise "Illegal argument!"
|
17
|
+
end
|
18
|
+
|
19
|
+
routes = Array.wrap(options[:routes]).flatten.compact.uniq.join(',')
|
20
|
+
stations = Array.wrap(options[:stations]).flatten.compact.uniq
|
21
|
+
|
22
|
+
if stations.size > 1
|
23
|
+
raise "Can only specify one station!"
|
24
|
+
end
|
25
|
+
|
26
|
+
connection.get('routes.aspx', { :type => options[:type], :routeid => routes, :stationid => stations.first })
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.alerts!(options = {})
|
30
|
+
allowed_keys = [:active, :accessibility, :planned, :routes, :station, :recent_days, :before]
|
31
|
+
if options.keys.any? { |k| !allowed_keys.include?(k) }
|
32
|
+
raise "Illegal argument!"
|
33
|
+
end
|
34
|
+
|
35
|
+
params = {}
|
36
|
+
params.merge!({ :activeonly => options[:active] }) if options[:active]
|
37
|
+
params.merge!({ :accessibility => options[:accessiblity] }) if options[:accessibility]
|
38
|
+
params.merge!({ :planned => options[:planned] }) if options[:planned]
|
39
|
+
|
40
|
+
routes = Array.wrap(options[:routes]).flatten.compact.uniq
|
41
|
+
stations = Array.wrap(options[:station]).flatten.compact.uniq
|
42
|
+
|
43
|
+
if stations.size > 1
|
44
|
+
raise "Can only specify one station!"
|
45
|
+
end
|
46
|
+
|
47
|
+
if routes.any? && stations.any?
|
48
|
+
raise "Cannot use route and station together!"
|
49
|
+
end
|
50
|
+
|
51
|
+
if options[:recent_days] && options[:before]
|
52
|
+
raise "Cannot use recent_days and before together!"
|
53
|
+
end
|
54
|
+
|
55
|
+
params.merge!({ :stationid => stations.first }) if stations.any?
|
56
|
+
params.merge!({ :routeid => routes.join(',') }) if routes.any?
|
57
|
+
params.merge!({ :recentdays => options[:recent_days] }) if options[:recent_days]
|
58
|
+
params.merge!({ :bystartdate => options[:before] }) if options[:before]
|
59
|
+
|
60
|
+
connection.get('alerts.aspx', params)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.debug
|
64
|
+
!!@debug
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.debug=(debug)
|
68
|
+
@debug = debug
|
69
|
+
@connection = nil
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module CTA
|
2
|
+
class BusTracker
|
3
|
+
class Parser < Faraday::Response::Middleware
|
4
|
+
def initialize(app, debug)
|
5
|
+
@debug = debug
|
6
|
+
super(app)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(request_env)
|
10
|
+
api_response = nil
|
11
|
+
@app.call(request_env).on_complete do |response_env|
|
12
|
+
parsed_body = ::MultiXml.parse(response_env.body)
|
13
|
+
|
14
|
+
if has_errors?(parsed_body)
|
15
|
+
api_response = CTA::API::Response.new(parsed_body, response_env.body, @debug)
|
16
|
+
else
|
17
|
+
case response_env.url.to_s
|
18
|
+
when /bustime\/.+\/getvehicles/
|
19
|
+
api_response = VehiclesResponse.new(parsed_body, response_env.body, @debug)
|
20
|
+
when /bustime\/.+\/gettime/
|
21
|
+
api_response = TimeResponse.new(parsed_body, response_env.body, @debug)
|
22
|
+
when /bustime\/.+\/getroutes/
|
23
|
+
api_response = RoutesResponse.new(parsed_body, response_env.body, @debug)
|
24
|
+
when /bustime\/.+\/getdirections/
|
25
|
+
api_response = DirectionsResponse.new(parsed_body, response_env.body, @debug)
|
26
|
+
when /bustime\/.+\/getstops/
|
27
|
+
api_response = StopsResponse.new(parsed_body, response_env.body, @debug)
|
28
|
+
when /bustime\/.+\/getpatterns/
|
29
|
+
api_response = PatternsResponse.new(parsed_body, response_env.body, @debug)
|
30
|
+
when /bustime\/.+\/getpredictions/
|
31
|
+
api_response = PredictionsResponse.new(parsed_body, response_env.body, @debug)
|
32
|
+
when /bustime\/.+\/getservicebulletins/
|
33
|
+
api_response = ServiceBulletinsResponse.new(parsed_body, response_env.body, @debug)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
api_response
|
39
|
+
end
|
40
|
+
|
41
|
+
def has_errors?(parsed_body)
|
42
|
+
!parsed_body.has_key?("bustime_response") || parsed_body["bustime_response"].has_key?("error")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module CTA
|
2
|
+
class CustomerAlerts
|
3
|
+
class Parser < Faraday::Response::Middleware
|
4
|
+
def initialize(app, debug)
|
5
|
+
@debug = debug
|
6
|
+
super(app)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(request_env)
|
10
|
+
api_response = nil
|
11
|
+
|
12
|
+
@app.call(request_env).on_complete do |response_env|
|
13
|
+
parsed_body = ::MultiXml.parse(response_env.body)
|
14
|
+
|
15
|
+
if has_errors?(parsed_body)
|
16
|
+
api_response = CTA::API::Response.new(parsed_body, response_env.body, @debug)
|
17
|
+
else
|
18
|
+
case response_env.url.to_s
|
19
|
+
when /routes\.aspx/
|
20
|
+
api_response = RouteStatusResponse.new(parsed_body, response_env.body, @debug)
|
21
|
+
when /alerts\.aspx/
|
22
|
+
api_response = AlertsResponse.new(parsed_body, response_env.body, @debug)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
api_response
|
28
|
+
end
|
29
|
+
|
30
|
+
def has_errors?(parsed_body)
|
31
|
+
if parsed_body["CTARoutes"]
|
32
|
+
Array.wrap(parsed_body["CTARoutes"]["ErrorCode"]).flatten.compact.uniq.first.to_i != 0
|
33
|
+
else
|
34
|
+
Array.wrap(parsed_body["CTAAlerts"]["ErrorCode"]).flatten.compact.uniq.first.to_i != 0
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class SimpleCache
|
2
|
+
def initialize(cache)
|
3
|
+
@cache = cache
|
4
|
+
end
|
5
|
+
|
6
|
+
def fetch(name, options = nil)
|
7
|
+
entry = read(name, options)
|
8
|
+
|
9
|
+
if !entry && block_given?
|
10
|
+
entry = yield
|
11
|
+
write(name, entry)
|
12
|
+
elsif !entry
|
13
|
+
entry = read(name, options)
|
14
|
+
end
|
15
|
+
|
16
|
+
entry
|
17
|
+
end
|
18
|
+
|
19
|
+
def read(name, options = nil)
|
20
|
+
entry = @cache[name]
|
21
|
+
|
22
|
+
if entry && entry[:expiration] < Time.now
|
23
|
+
entry = @cache[name] = nil
|
24
|
+
end
|
25
|
+
|
26
|
+
entry ? entry[:value] : entry
|
27
|
+
end
|
28
|
+
|
29
|
+
def write(name, value)
|
30
|
+
@cache[name] = { :expiration => (Time.now + 60), :value => value }
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module CTA
|
2
|
+
class TrainTracker
|
3
|
+
class Parser < Faraday::Response::Middleware
|
4
|
+
def initialize(app, debug)
|
5
|
+
@debug = debug
|
6
|
+
super(app)
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(request_env)
|
10
|
+
api_response = nil
|
11
|
+
|
12
|
+
@app.call(request_env).on_complete do |response_env|
|
13
|
+
parsed_body = ::MultiXml.parse(response_env.body)
|
14
|
+
|
15
|
+
if has_errors?(parsed_body)
|
16
|
+
api_response = CTA::API::Response.new(parsed_body, response_env.body, @debug)
|
17
|
+
else
|
18
|
+
case response_env.url.to_s
|
19
|
+
when /ttarrivals\.aspx/
|
20
|
+
api_response = ArrivalsResponse.new(parsed_body, response_env.body, @debug)
|
21
|
+
when /ttfollow\.aspx/
|
22
|
+
api_response = FollowResponse.new(parsed_body, response_env.body, @debug)
|
23
|
+
when /ttpositions\.aspx/
|
24
|
+
api_response = PositionsResponse.new(parsed_body, response_env.body, @debug)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
api_response
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_errors?(parsed_body)
|
33
|
+
parsed_body["ctatt"]["errCd"] != "0"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module CTA
|
2
|
+
class Bus < CTA::Trip
|
3
|
+
def predictions!(options = {})
|
4
|
+
opts = (self.vehicle_id ? { :vehicles => self.vehicle_id } : { :routes => self.route_id })
|
5
|
+
puts opts
|
6
|
+
CTA::BusTracker.predictions!(options.merge(opts))
|
7
|
+
end
|
8
|
+
|
9
|
+
def live!(position, predictions = [])
|
10
|
+
class << self
|
11
|
+
attr_reader :lat, :lon, :vehicle_id, :heading, :pattern_id, :pattern_distance, :route, :delayed, :speed, :predictions
|
12
|
+
end
|
13
|
+
|
14
|
+
@lat = position["lat"].to_f
|
15
|
+
@lon = position["lon"].to_f
|
16
|
+
@heading = position["hdg"].to_i
|
17
|
+
@vehicle_id = position["vid"].to_i
|
18
|
+
@pattern_id = position["pid"].to_i
|
19
|
+
@pattern_distance = position["pdist"].to_i
|
20
|
+
@route = CTA::Route.where(:route_id => position["rt"]).first
|
21
|
+
@delayed = (position["dly"] == "true")
|
22
|
+
@speed = position["spd"].to_i
|
23
|
+
|
24
|
+
@predictions = Array.wrap(predictions).map { |p| Prediction.new(p) }
|
25
|
+
end
|
26
|
+
|
27
|
+
class Prediction
|
28
|
+
attr_reader :type, :stop, :distance, :route, :direction, :destination,
|
29
|
+
:prediction_generated_at, :arrival_time, :delayed, :minutes, :seconds
|
30
|
+
|
31
|
+
def initialize(data)
|
32
|
+
@type = data["typ"]
|
33
|
+
@stop = CTA::Stop.where(:stop_id => data["stpid"]).first || CTA::Stop.new_from_api_response(data)
|
34
|
+
@distance = data["dstp"].to_i
|
35
|
+
@route = CTA::Route.where(:route_id => data["rt"]).first
|
36
|
+
@direction = CTA::BusTracker::Direction.new(data["rtdir"])
|
37
|
+
@destination = data["des"]
|
38
|
+
@prediction_generated_at = DateTime.parse(data["tmstmp"])
|
39
|
+
@arrival_time = DateTime.parse(data["prdtm"])
|
40
|
+
@seconds = @arrival_time.to_time - @prediction_generated_at.to_time
|
41
|
+
@minutes = (@seconds / 60).ceil
|
42
|
+
@delayed = (data["dly"] == "true")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module CTA
|
2
|
+
class Route < Sequel::Model
|
3
|
+
set_primary_key :route_id
|
4
|
+
|
5
|
+
one_to_many :trips, :key => :route_id
|
6
|
+
|
7
|
+
def self.[](*args)
|
8
|
+
potential_route = args.first.downcase.to_sym
|
9
|
+
if CTA::Train::FRIENDLY_L_ROUTES.has_key?(potential_route)
|
10
|
+
super(Array.wrap(CTA::Train::FRIENDLY_L_ROUTES[potential_route].capitalize))
|
11
|
+
else
|
12
|
+
super(args)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def stops
|
17
|
+
# Gosh, I wish SQLite could do "SELECT DISTINCT ON..."
|
18
|
+
CTA::Stop.with_sql(<<-SQL)
|
19
|
+
SELECT s.*
|
20
|
+
FROM stops s
|
21
|
+
WHERE s.stop_id IN (
|
22
|
+
SELECT DISTINCT st.stop_id
|
23
|
+
FROM stop_times st
|
24
|
+
JOIN trips t ON st.trip_id = t.trip_id
|
25
|
+
WHERE t.route_id = '#{self.route_id}'
|
26
|
+
)
|
27
|
+
SQL
|
28
|
+
end
|
29
|
+
|
30
|
+
def live!(vehicles)
|
31
|
+
class << self
|
32
|
+
attr_reader :vehicles
|
33
|
+
end
|
34
|
+
|
35
|
+
@vehicles = vehicles
|
36
|
+
end
|
37
|
+
|
38
|
+
def predictions!(options = {})
|
39
|
+
if CTA::Train::L_ROUTES.keys.include?(self.route_id.downcase)
|
40
|
+
CTA::TrainTracker.predictions!(options.merge({:route => self.route_id.downcase}))
|
41
|
+
else
|
42
|
+
CTA::BusTracker.predictions!(options.merge({:route => self.route_id}))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def locations!(options = {})
|
47
|
+
if CTA::Train::L_ROUTES.keys.include?(self.route_id.downcase)
|
48
|
+
CTA::TrainTracker.locations!(options.merge({:routes => self.route_id.downcase}))
|
49
|
+
else
|
50
|
+
raise "CTA BusTracker has no direct analog of the TrainTracker locations api. Try predictions instead."
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def status!
|
55
|
+
CTA::CustomerAlerts.status!(:routes => self.route_id).routes.first
|
56
|
+
end
|
57
|
+
|
58
|
+
def alerts!
|
59
|
+
CTA::CustomerAlerts.alerts!(:route => self.route_id).alerts
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module CTA
|
2
|
+
class Stop < Sequel::Model
|
3
|
+
set_primary_key :stop_id
|
4
|
+
|
5
|
+
one_to_many :child_stops, :class => self, :key => :parent_station
|
6
|
+
many_to_one :parent_stop, :class => self, :key => :parent_station
|
7
|
+
|
8
|
+
one_to_many :transfers_from, :class => 'CTA::Transfer', :key => :from_stop_id
|
9
|
+
one_to_many :transfers_to, :class => 'CTA::Transfer', :key => :to_stop_id
|
10
|
+
|
11
|
+
many_to_many :trips, :left_key => :stop_id, :right_key => :trip_id, :join_table => :stop_times
|
12
|
+
|
13
|
+
def routes
|
14
|
+
CTA::Route.with_sql(<<-SQL)
|
15
|
+
SELECT r.*
|
16
|
+
FROM routes r
|
17
|
+
WHERE r.route_id IN (
|
18
|
+
SELECT DISTINCT t.route_id
|
19
|
+
FROM stop_times st
|
20
|
+
JOIN trips t ON st.trip_id = t.trip_id
|
21
|
+
WHERE st.stop_id = '#{self.stop_id}'
|
22
|
+
)
|
23
|
+
SQL
|
24
|
+
end
|
25
|
+
|
26
|
+
# Some CTA routes are seasonal, and are missing from the GTFS feed.
|
27
|
+
# However, the API still returns that info. So, we create a dummy CTA::Stop
|
28
|
+
# to fill in the gaps. I've emailed CTA developer support for clarification.
|
29
|
+
def self.new_from_api_response(s)
|
30
|
+
CTA::Stop.unrestrict_primary_key
|
31
|
+
stop = CTA::Stop.new({
|
32
|
+
:stop_id => s["stpid"].to_i,
|
33
|
+
:stop_name => s["stpnm"],
|
34
|
+
:stop_lat => s["lat"].to_f,
|
35
|
+
:stop_lon => s["lon"].to_f,
|
36
|
+
:location_type => 3, # Bus in GTFS-land
|
37
|
+
:stop_desc => "#{s["stpnm"]} (seasonal, generated from API results - missing from GTFS feed)"
|
38
|
+
})
|
39
|
+
CTA::Stop.restrict_primary_key
|
40
|
+
|
41
|
+
stop
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop_type
|
45
|
+
if self.stop_id < 30000
|
46
|
+
:bus
|
47
|
+
elsif self.stop_id < 40000
|
48
|
+
:rail
|
49
|
+
else
|
50
|
+
:parent_station
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def predictions!(options = {})
|
55
|
+
if self.stop_type == :bus
|
56
|
+
CTA::BusTracker.predictions!(options.merge({:stops => self.stop_id}))
|
57
|
+
else
|
58
|
+
if self.stop_type == :rail
|
59
|
+
CTA::TrainTracker.predictions!(options.merge({:station => self.stop_id}))
|
60
|
+
else
|
61
|
+
CTA::TrainTracker.predictions!(options.merge({:parent_station => self.stop_id}))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module CTA
|
2
|
+
class Train < CTA::Trip
|
3
|
+
L_ROUTES = {
|
4
|
+
"red" => { :name => "Red",
|
5
|
+
:directions => { "1" => "Howard-bound", "5" => "95th/Dan Ryan-bound" }
|
6
|
+
},
|
7
|
+
"blue" => { :name => "Blue",
|
8
|
+
:directions => { "1" => "O'Hare-bound", "5" => "Forest Park-bound" }
|
9
|
+
},
|
10
|
+
"brn" => { :name => "Brown",
|
11
|
+
:directions => { "1" => "Kimball-bound", "5" => "Loop-bound" }
|
12
|
+
},
|
13
|
+
"g" => { :name => "Green",
|
14
|
+
:directions => { "1" => "Harlem/Lake-bound", "5" => "Ashland/63rd- or Cottage Grove-bound (toward 63rd St destinations)" }
|
15
|
+
},
|
16
|
+
"org" => { :name => "Orange",
|
17
|
+
:directions => { "1" => "Loop-bound", "5" => "Midway-bound" }
|
18
|
+
},
|
19
|
+
"p" => { :name => "Purple",
|
20
|
+
:directions => { "1" => "Linden-bound", "5" => "Howard- or Loop-bound" }
|
21
|
+
},
|
22
|
+
"pink" => { :name => "Pink",
|
23
|
+
:directions => { "1" => "Loop-bound", "5" => "54th/Cermak-bound" }
|
24
|
+
},
|
25
|
+
"y" => { :name => "Yellow",
|
26
|
+
:directions => { "1" => "Skokie-bound", "5" => "Howard-bound" }
|
27
|
+
},
|
28
|
+
}
|
29
|
+
FRIENDLY_L_ROUTES = Hash[L_ROUTES.values.map { |r| r[:name].downcase.to_sym }.zip(L_ROUTES.keys)]
|
30
|
+
|
31
|
+
def follow!
|
32
|
+
CTA::TrainTracker.follow!(:run => self.schd_trip_id.gsub("R", ""))
|
33
|
+
end
|
34
|
+
|
35
|
+
def live!(position, predictions)
|
36
|
+
class << self
|
37
|
+
attr_reader :lat, :lon, :heading, :predictions
|
38
|
+
end
|
39
|
+
|
40
|
+
@lat = position["lat"].to_f
|
41
|
+
@lon = position["lon"].to_f
|
42
|
+
@heading = position["heading"].to_i
|
43
|
+
|
44
|
+
@predictions = Array.wrap(predictions).map { |p| Prediction.new(p) }
|
45
|
+
end
|
46
|
+
|
47
|
+
class Prediction
|
48
|
+
attr_reader :run, :trip, :destination, :direction, :next_station,
|
49
|
+
:prediction_generated_at, :arrival_time, :minutes, :seconds,
|
50
|
+
:approaching, :scheduled, :delayed, :flags, :route, :lat, :lon,
|
51
|
+
:heading, :route, :direction
|
52
|
+
|
53
|
+
def initialize(data)
|
54
|
+
@run = data["rn"]
|
55
|
+
@trip = CTA::Trip.where(:schd_trip_id => "R#{@run}").first
|
56
|
+
@destination = CTA::Stop.where(:stop_id => data["destSt"]).first
|
57
|
+
@next_station = CTA::Stop.where(:stop_id => (data["staId"] || data["nextStaId"])).first
|
58
|
+
@prediction_generated_at = DateTime.parse(data["prdt"])
|
59
|
+
@arrival_time = DateTime.parse(data["arrT"])
|
60
|
+
@seconds = @arrival_time.to_time - @prediction_generated_at.to_time
|
61
|
+
@minutes = (@seconds / 60).ceil
|
62
|
+
@approaching = (data["isApp"] == "1")
|
63
|
+
@delayed = (data["isDly"] == "1")
|
64
|
+
@scheduled = (data["isSch"] == "1")
|
65
|
+
@flags = data["flags"]
|
66
|
+
@lat = data["lat"].to_f
|
67
|
+
@lon = data["lon"].to_f
|
68
|
+
@heading = data["heading"].to_i
|
69
|
+
@route = @trip.route
|
70
|
+
@direction = L_ROUTES[@route.route_id.downcase][:directions][data["trDr"]]
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|