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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 69b3ea9908066bc8bdbf52865df0885b2c7bc5a3
4
+ data.tar.gz: 60ec4638cb25ad8a65e1282252bc630972c9fa8f
5
+ SHA512:
6
+ metadata.gz: f77382a34fb99fce097142a98c22c3bed8a25992e31279ea18887c3a0398c97346717138019486d5cfdebbefe67a0f3173a517383d8021d516faa8366e8efcbd
7
+ data.tar.gz: 513d7dca04757788ebc544f1437b852094f9ab1e13c1331380395cc7a369c5d82f97022879052fbfb4bcd6842433fe6c8d15f57e1a66d49597fde5cfd2085200
data/.gitignore ADDED
@@ -0,0 +1,37 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /test/tmp/
9
+ /test/version_tmp/
10
+ /tmp/
11
+ *.swp
12
+
13
+ ## Specific to RubyMotion:
14
+ .dat*
15
+ .repl_history
16
+ build/
17
+
18
+ ## Documentation cache and generated files:
19
+ /.yardoc/
20
+ /_yardoc/
21
+ /doc/
22
+ /rdoc/
23
+
24
+ ## Environment normalisation:
25
+ /.bundle/
26
+ /lib/bundler/man/
27
+
28
+ # for a library or gem, you might want to ignore these files since the code is
29
+ # intended to run in multiple environments; otherwise, check them in:
30
+ Gemfile.lock
31
+ .ruby-version
32
+ .ruby-gemset
33
+
34
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
35
+ .rvmrc
36
+
37
+ /data/cta-gtfs.db
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in cta-api-redux.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Andrew Hayworth
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # cta_redux
2
+
3
+ # Reloading CTA GTFS data
4
+
5
+ Note that this will take a long time - there are several million stop_time records.
6
+
7
+ 1. cd data && curl 'http://www.transitchicago.com/downloads/sch_data/<latest file>' > gtfs.zip && unzip gtfs.zip
8
+
9
+ 2. cd ../script && for i in `ls ../data/*.txt`; do echo $i; ./gtfs_to_sqlite.rb $i ../data/cta-gtfs.db; done
10
+
11
+ 3. rm ../data/*{txt,htm,zip}
12
+
13
+ 4. cd ../data && gzip cta-gtfs.db
14
+
15
+ 5. Commit / push / create release and gem
16
+
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
data/cta_redux.gemspec ADDED
@@ -0,0 +1,32 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cta_redux/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "cta_redux"
8
+ spec.version = CTA::VERSION
9
+ spec.authors = ["Andrew Hayworth"]
10
+ spec.email = ["ahayworth@gmail.com"]
11
+ spec.summary = %q{A clean, integrated API for CTA BusTracker, TrainTracker, customer alerts, and GTFS data.}
12
+ spec.description = %q{cta_redux takes the data provided by the CTA in its various forms, and turns it into a clean,
13
+ cohesive client API that can be used to easily build a transit related project. Using Sequel,
14
+ we integrate GTFS scheduled service data with live data provided by the CTA's various APIs (like
15
+ BusTracker, TrainTracker, and the CTA customer alerts feed.}
16
+ spec.homepage = "http://ctaredux.ahayworth.com"
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files -z`.split("\x0")
20
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_development_dependency "bundler", "~> 1.7"
25
+ spec.add_development_dependency "rake", "~> 10.0"
26
+ spec.add_development_dependency "rspec", "~> 3.2.0"
27
+ spec.add_dependency "sequel", ">= 4.19.0"
28
+ spec.add_dependency "sqlite3", ">= 1.3.10"
29
+ spec.add_dependency "faraday", ">= 0.9.1"
30
+ spec.add_dependency "faraday_middleware", ">= 0.9.1"
31
+ spec.add_dependency "multi_xml", ">= 0.5.5"
32
+ end
data/data/.gitkeep ADDED
File without changes
Binary file
@@ -0,0 +1,45 @@
1
+ module CTA
2
+ class API
3
+ class Error
4
+ attr_reader :code
5
+ attr_reader :message
6
+
7
+ def initialize(options = {})
8
+ @message = options[:message] || "OK"
9
+ @code = options[:code] ? options[:code].to_i : (@message == "OK" ? 0 : 1)
10
+ end
11
+ end
12
+
13
+ class Response
14
+ attr_reader :timestamp
15
+ attr_reader :error
16
+ attr_reader :raw_body
17
+ attr_reader :parsed_body
18
+
19
+ def initialize(parsed_body, raw_body, debug)
20
+ if parsed_body["bustime_response"]
21
+ @timestamp = DateTime.now
22
+ if parsed_body["bustime_response"].has_key?("error")
23
+ @error = Error.new({ :message => parsed_body["bustime_response"]["error"]["msg"] })
24
+ else
25
+ @error = Error.new
26
+ end
27
+ elsif parsed_body["ctatt"]
28
+ @timestamp = DateTime.parse(parsed_body["ctatt"]["tmst"])
29
+ @error = Error.new({ :code => parsed_body["ctatt"]["errCd"], :message => parsed_body["ctatt"]["errNm"] })
30
+ else # CustomerAlert
31
+ key = parsed_body["CTARoutes"] ? "CTARoutes" : "CTAAlerts"
32
+ code = Array.wrap(parsed_body[key]["ErrorCode"]).flatten.compact.uniq.first
33
+ msg = Array.wrap(parsed_body[key]["ErrorMessage"]).flatten.compact.uniq.first
34
+ @timestamp = DateTime.parse(parsed_body[key]["TimeStamp"])
35
+ @error = Error.new({ :code => code, :message => msg })
36
+ end
37
+
38
+ if debug
39
+ @parsed_body = parsed_body
40
+ @raw_body = raw_body
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,178 @@
1
+ require 'date'
2
+ require 'faraday'
3
+ require 'multi_xml'
4
+
5
+ module CTA
6
+ class BusTracker
7
+
8
+ class VehiclesResponse < CTA::API::Response
9
+ attr_reader :vehicles
10
+
11
+ def initialize(parsed_body, raw_body, debug)
12
+ super(parsed_body, raw_body, debug)
13
+ @vehicles = Array.wrap(parsed_body["bustime_response"]["vehicle"]).map do |v|
14
+ bus = CTA::Bus.find_active_run(v["rt"], v["tmstmp"], (v["dly"] == "true")).first
15
+ bus.live!(v)
16
+
17
+ bus
18
+ end
19
+ end
20
+ end
21
+
22
+ class TimeResponse < CTA::API::Response
23
+ attr_reader :timestamp
24
+
25
+ def initialize(parsed_body, raw_body, debug)
26
+ super(parsed_body, raw_body, debug)
27
+ @timestamp = DateTime.parse(parsed_body["bustime_response"]["tm"])
28
+ end
29
+ end
30
+
31
+ class RoutesResponse < CTA::API::Response
32
+ attr_reader :routes
33
+
34
+ def initialize(parsed_body, raw_body, debug)
35
+ super(parsed_body, raw_body, debug)
36
+ @routes = Array.wrap(parsed_body["bustime_response"]["route"]).map do |r|
37
+ rt = CTA::Route.where(:route_id => r["rt"]).first
38
+ rt.route_color = r["rtclr"]
39
+
40
+ rt
41
+ end
42
+ end
43
+ end
44
+
45
+ class DirectionsResponse < CTA::API::Response
46
+ attr_reader :directions
47
+
48
+ def initialize(parsed_body, raw_body, debug)
49
+ super(parsed_body, raw_body, debug)
50
+ @directions = Array.wrap(parsed_body["bustime_response"]["dir"]).map { |d| Direction.new(d) }
51
+ end
52
+ end
53
+
54
+ class StopsResponse < CTA::API::Response
55
+ attr_reader :stops
56
+
57
+ def initialize(parsed_body, raw_body, debug)
58
+ super(parsed_body, raw_body, debug)
59
+ @stops = Array.wrap(parsed_body["bustime_response"]["stop"]).map do |s|
60
+ CTA::Stop.where(:stop_id => s["stpid"]).first || CTA::Stop.new_from_api_response(s)
61
+ end
62
+ end
63
+ end
64
+
65
+ class PatternsResponse < CTA::API::Response
66
+ attr_reader :patterns
67
+
68
+ def initialize(parsed_body, raw_body, debug)
69
+ super(parsed_body, raw_body, debug)
70
+ @patterns = Array.wrap(parsed_body["bustime_response"]["ptr"]).map { |p| Pattern.new(p) }
71
+ end
72
+ end
73
+
74
+ class PredictionsResponse < CTA::API::Response
75
+ attr_reader :vehicles
76
+ attr_reader :predictions
77
+
78
+ def initialize(parsed_body, raw_body, debug)
79
+ super(parsed_body, raw_body, debug)
80
+ @vehicles = Array.wrap(parsed_body["bustime_response"]["prd"]).map do |p|
81
+ bus = CTA::Bus.find_active_run(p["rt"], p["tmstmp"], (p["dly"] == "true")).first
82
+ bus.live!(p, p)
83
+
84
+ bus
85
+ end
86
+ @predictions = @vehicles.map(&:predictions).flatten
87
+ end
88
+ end
89
+
90
+ class ServiceBulletinsResponse < CTA::API::Response
91
+ attr_reader :bulletins
92
+
93
+ def initialize(parsed_body, raw_body, debug)
94
+ super(parsed_body, raw_body, debug)
95
+ @bulletins = Array.wrap(parsed_body["bustime_response"]["sb"]).map { |sb| ServiceBulletin.new(sb) }
96
+ end
97
+ end
98
+
99
+ class ServiceBulletin
100
+ attr_reader :name, :subject, :details, :brief, :priority, :affected_services
101
+
102
+ def initialize(sb)
103
+ @name = sb["nm"]
104
+ @subject = sb["sbj"]
105
+ @details = sb["dtl"]
106
+ @brief = sb["brf"]
107
+ @priority = sb["prty"].downcase.to_sym
108
+
109
+ @affected_services = Array.wrap(sb["srvc"]).map { |svc| Service.new(svc) }
110
+ end
111
+ end
112
+
113
+ class Service
114
+ attr_reader :route
115
+ attr_reader :direction
116
+ attr_reader :stop
117
+ attr_reader :stop_name
118
+
119
+ def initialize(s)
120
+ @route = CTA::Route.where(:route_id => s["rt"]).first
121
+ @direction = Direction.new(s["rtdir"]) if s["rtdir"]
122
+ if s["stpid"]
123
+ @stop = CTA::Stop.where(:stop_id => s["stpid"]).first || CTA::Stop.new_from_api_response(s)
124
+ @stop_name = @stop.name
125
+ else
126
+ @stop_name = s["stpnm"] # ugh
127
+ end
128
+ end
129
+
130
+ def predictions!
131
+ options = { :route => self.route }
132
+ options.merge!({ :stop => self.stop_id }) if self.stop_id
133
+ CTA::BusTracker.predictions!(options)
134
+ end
135
+ end
136
+
137
+ class Pattern
138
+ attr_reader :id
139
+ attr_reader :pattern_id
140
+ attr_reader :length
141
+ attr_reader :direction
142
+ attr_reader :points
143
+
144
+ def initialize(p)
145
+ @id = @pattern_id = p["pid"].to_i
146
+ @length = p["ln"].to_f
147
+ @direction = Direction.new(p["rtdir"])
148
+
149
+ @points = Array.wrap(p["pt"]).map { |pnt| Point.new(pnt) }
150
+ end
151
+ end
152
+
153
+ class Point
154
+ attr_reader :sequence, :lat, :lon, :latitude, :longitude, :type, :stop, :distance
155
+
156
+ def initialize(p)
157
+ @sequence = p["seq"].to_i
158
+ @lat = @latitude = p["lat"].to_f
159
+ @lon = @longitude = p["lon"].to_f
160
+ @type = (p["typ"] == "S" ? :stop : :waypoint)
161
+ @stop = CTA::Stop.where(:stop_id => p["stpid"]).first || CTA::Stop.new_from_api_response(p)
162
+ @distance = p["pdist"].to_f if p["pdist"]
163
+ end
164
+
165
+ def <=>(other)
166
+ self.sequence <=> other.sequence
167
+ end
168
+ end
169
+
170
+ class Direction
171
+ attr_reader :direction
172
+
173
+ def initialize(d)
174
+ @direction = d
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,68 @@
1
+ require 'date'
2
+ require 'faraday'
3
+ require 'multi_xml'
4
+
5
+ module CTA
6
+ class CustomerAlerts
7
+ class RouteStatus
8
+ attr_reader :route, :route_color, :route_text_color, :service_id, :route_url, :status, :status_color
9
+
10
+ def initialize(s)
11
+ @route = CTA::Route.where(:route_id => s["Route"].split(" ").first).or(:route_id => s["ServiceId"]).first
12
+ @route_color = s["RouteColorCode"]
13
+ @route_text_color = s["RouteTextColor"]
14
+ @status = s["RouteStatus"]
15
+ @status_color = s["RouteStatusColor"]
16
+ end
17
+ end
18
+
19
+ class Alert
20
+ attr_reader :id, :alert_id, :headline, :short_description, :full_description, :score,
21
+ :severity_color, :category, :impact, :start, :end, :tbd, :major_alert, :is_major_alert,
22
+ :url, :services
23
+
24
+ def initialize(a)
25
+ @id = @alert_id = a["AlertId"].to_i
26
+ @headline = a["Headline"]
27
+ @short_description = a["ShortDescription"]
28
+ @full_description = a["FullDescription"]
29
+ @score = a["SeverityScore"].to_i
30
+ @severity_color = a["SeverityColor"]
31
+ @category = a["SeverityCSS"].downcase.to_sym
32
+ @impact = a["Impact"]
33
+ @start = DateTime.parse(a["EventStart"]) if a["EventStart"]
34
+ @end = DateTime.parse(a["EventEnd"]) if a["EventEnd"]
35
+ @tbd = (a["TBD"] == "1")
36
+ @major_alert = @is_major_alert = (a["MajorAlert"] == "1")
37
+ @url = a["AlertURL"]
38
+
39
+ @services = Array.wrap(a["ImpactedService"]["Service"]).map do |s|
40
+ CTA::Route.where(:route_id => s["ServiceName"].split(" ")).or(:route_id => s["ServiceId"]).first
41
+ end
42
+ end
43
+
44
+ def major?
45
+ @major_alert
46
+ end
47
+ end
48
+
49
+ class AlertsResponse < CTA::API::Response
50
+ attr_reader :alerts
51
+
52
+ def initialize(parsed_body, raw_body, debug)
53
+ super(parsed_body, raw_body, debug)
54
+ @alerts = Array.wrap(parsed_body["CTAAlerts"]["Alert"]).map { |a| Alert.new(a) }
55
+ end
56
+ end
57
+
58
+ class RouteStatusResponse < CTA::API::Response
59
+ attr_reader :routes
60
+
61
+ def initialize(parsed_body, raw_body, debug)
62
+ super(parsed_body, raw_body, debug)
63
+ @routes = Array.wrap(parsed_body["CTARoutes"]["RouteInfo"]).map { |r| RouteStatus.new(r) }
64
+ end
65
+ end
66
+
67
+ end
68
+ end
@@ -0,0 +1,89 @@
1
+ module CTA
2
+ class TrainTracker
3
+ class ArrivalsResponse < CTA::API::Response
4
+ attr_reader :routes, :trains, :predictions
5
+
6
+ def initialize(parsed_body, raw_body, debug)
7
+ super(parsed_body, raw_body, debug)
8
+
9
+ eta_map = Array.wrap(parsed_body["ctatt"]["eta"]).inject({}) do |h, eta|
10
+ h[eta["rt"]] ||= []
11
+ h[eta["rt"]] << eta
12
+
13
+ h
14
+ end
15
+
16
+ @routes = eta_map.map do |rt, etas|
17
+ trains = etas.map do |t|
18
+ train = CTA::Train.find_active_run(t["rn"], self.timestamp, (t["isDly"] == "1")).first
19
+ if !train
20
+ train = CTA::Train.find_active_run(t["rn"], self.timestamp, true).first
21
+ end
22
+ position = t.select { |k,v| ["lat", "lon", "heading"].include?(k) }
23
+ train.live!(position, t)
24
+
25
+ train
26
+ end
27
+
28
+ route = CTA::Route.where(:route_id => rt.capitalize).first
29
+ route.live!(trains)
30
+
31
+ route
32
+ end
33
+
34
+ @trains = @routes.map(&:vehicles).flatten
35
+ @predictions = @trains.map(&:predictions).flatten
36
+ end
37
+ end
38
+
39
+ class FollowResponse < CTA::API::Response
40
+ attr_reader :train, :predictions
41
+
42
+ def initialize(parsed_body, raw_body, debug)
43
+ super(parsed_body, raw_body, debug)
44
+
45
+ train_info = Array.wrap(parsed_body["ctatt"]["eta"]).first
46
+ @train = CTA::Train.find_active_run(train_info["rn"], self.timestamp, (train_info["isDly"] == "1")).first
47
+ if !@train
48
+ @train = CTA::Train.find_active_run(train_info["rn"], self.timestamp, true).first
49
+ end
50
+ @train.live!(parsed_body["ctatt"]["position"], parsed_body["ctatt"]["eta"])
51
+ @predictions = @train.predictions
52
+ end
53
+ end
54
+
55
+ class PositionsResponse < CTA::API::Response
56
+ attr_reader :routes, :trains, :predictions
57
+
58
+ def initialize(parsed_body, raw_body, debug)
59
+ super(parsed_body, raw_body, debug)
60
+ @routes = Array.wrap(parsed_body["ctatt"]["route"]).map do |route|
61
+ rt = Route.where(:route_id => route["name"].capitalize).first
62
+
63
+ trains = Array.wrap(route["train"]).map do |train|
64
+ t = CTA::Train.find_active_run(train["rn"], self.timestamp, (train["isDly"] == "1")).first
65
+ if !t # Sometimes the CTA doesn't report things as delayed even when they ARE
66
+ t = CTA::Train.find_active_run(train["rn"], self.timestamp, true).first
67
+ end
68
+
69
+ if !t
70
+ puts "Couldn't find train #{train["rn"]} - this is likely a bug."
71
+ next
72
+ end
73
+
74
+ position = train.select { |k,v| ["lat", "lon", "heading"].include?(k) }
75
+ t.live!(position, train)
76
+
77
+ t
78
+ end
79
+
80
+ rt.live!(trains)
81
+ rt
82
+ end.compact
83
+
84
+ @trains = @routes.compact.map(&:vehicles).flatten
85
+ @predictions = @trains.compact.map(&:predictions).flatten
86
+ end
87
+ end
88
+ end
89
+ end