cta_redux 0.1.0

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