cta_redux 0.1.0

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +37 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE +22 -0
  6. data/README.md +16 -0
  7. data/Rakefile +2 -0
  8. data/cta_redux.gemspec +32 -0
  9. data/data/.gitkeep +0 -0
  10. data/data/cta-gtfs.db.gz +0 -0
  11. data/lib/cta_redux/api/api_response.rb +45 -0
  12. data/lib/cta_redux/api/bus_tracker.rb +178 -0
  13. data/lib/cta_redux/api/customer_alerts.rb +68 -0
  14. data/lib/cta_redux/api/train_tracker.rb +89 -0
  15. data/lib/cta_redux/bus_tracker.rb +183 -0
  16. data/lib/cta_redux/customer_alerts.rb +72 -0
  17. data/lib/cta_redux/faraday_middleware/bus_tracker_parser.rb +46 -0
  18. data/lib/cta_redux/faraday_middleware/customer_alerts_parser.rb +39 -0
  19. data/lib/cta_redux/faraday_middleware/simple_cache.rb +32 -0
  20. data/lib/cta_redux/faraday_middleware/train_tracker_parser.rb +37 -0
  21. data/lib/cta_redux/models/agency.rb +4 -0
  22. data/lib/cta_redux/models/bus.rb +46 -0
  23. data/lib/cta_redux/models/calendar.rb +7 -0
  24. data/lib/cta_redux/models/route.rb +62 -0
  25. data/lib/cta_redux/models/shape.rb +4 -0
  26. data/lib/cta_redux/models/stop.rb +66 -0
  27. data/lib/cta_redux/models/stop_time.rb +6 -0
  28. data/lib/cta_redux/models/train.rb +74 -0
  29. data/lib/cta_redux/models/transfer.rb +6 -0
  30. data/lib/cta_redux/models/trip.rb +64 -0
  31. data/lib/cta_redux/train_tracker.rb +103 -0
  32. data/lib/cta_redux/version.rb +3 -0
  33. data/lib/cta_redux.rb +50 -0
  34. data/script/gtfs_to_sqlite.rb +137 -0
  35. data/spec/bus_tracker_spec.rb +149 -0
  36. data/spec/customer_alerts_spec.rb +48 -0
  37. data/spec/spec_helper.rb +16 -0
  38. data/spec/stubs/alerts_response.xml +362 -0
  39. data/spec/stubs/getdirections_response.xml +16 -0
  40. data/spec/stubs/getpatterns_rt22_response.xml +2768 -0
  41. data/spec/stubs/getpredictions_rt22stpid15895_response.xml +59 -0
  42. data/spec/stubs/getpredictions_vid4361_response.xml +647 -0
  43. data/spec/stubs/getroutes_response.xml +774 -0
  44. data/spec/stubs/getservicebulletins_rt8_response.xml +110 -0
  45. data/spec/stubs/getstops_response.xml +614 -0
  46. data/spec/stubs/gettime_response.xml +2 -0
  47. data/spec/stubs/getvehicles_rt22_response.xml +239 -0
  48. data/spec/stubs/getvehicles_vid4394_response.xml +23 -0
  49. data/spec/stubs/route_status8_response.xml +1 -0
  50. data/spec/stubs/routes_response.xml +1 -0
  51. data/spec/stubs/ttarivals_stpid30141_response.xml +1 -0
  52. data/spec/stubs/ttfollow_run217_response.xml +1 -0
  53. data/spec/stubs/ttpositions_response.xml +1 -0
  54. data/spec/train_tracker_spec.rb +70 -0
  55. 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,4 @@
1
+ module CTA
2
+ class Agency < Sequel::Model(:agency)
3
+ end
4
+ 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,7 @@
1
+ module CTA
2
+ class Calendar < Sequel::Model(:calendar)
3
+ set_primary_key :service_id
4
+
5
+ one_to_many :trips, :key => :service_id
6
+ end
7
+ 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,4 @@
1
+ module CTA
2
+ class Shape < Sequel::Model
3
+ end
4
+ 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,6 @@
1
+ module CTA
2
+ class StopTime < Sequel::Model
3
+ many_to_one :trip, :key => :trip_id
4
+ many_to_one :stop, :key => :stop_id
5
+ end
6
+ 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