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
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