pius-wmata-hotfix 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Jeremy McAnally
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,49 @@
1
+ = wmata
2
+
3
+ Jeremy McAnally - Intridea
4
+
5
+ A nice little gem for accessing the WMATA data API (http://developer.wmata.com).
6
+
7
+ == Installing / Getting Started
8
+
9
+ First, install the gem...
10
+
11
+ gem install wmata
12
+
13
+ ...or use your Git clone. It's up to you. :)
14
+
15
+ Next, mosey on over to http://developer.wmata.com and get yourself a developer account. Create an application (it can just be named "test" or whatever for now) and get your API key. You'll need to provide your API key for any API work you'll be doing.
16
+
17
+ To get started using the gem, require it (of course), and then set your API key:
18
+
19
+ WMATA.api_key = "ab7bce7ba8e08ccca9ce80ab890eb8a9dd"
20
+
21
+ Now you're ready to rock and roll!
22
+
23
+ == Usage
24
+
25
+ The gem's functionality is primarily oriented around the +WMATA+ module. For example, to get all the rail lines available, you'd do this:
26
+
27
+ WMATA.lines
28
+
29
+ This will give you an array of +Line+ objects to manipulate. So, you could do something like this:
30
+
31
+ WMATA.lines.first.incidents
32
+
33
+ This code would give you an array of +Incident+ objects that you can manipulate. Essentially, its usage boils down to a simple maxim: most of the fetching logic should be accessed through the +WMATA+ module or the domain objects returned from methods called on methods called on +WMATA+. You can call the fetching logic directly (e.g., +Line.get_all+), but it's smarter to use the public API.
34
+
35
+ For detailed information about what methods are available, see the documentation for +WMATA+ and each individual resource class.
36
+
37
+ == Note on Patches/Pull Requests
38
+
39
+ * Fork the project.
40
+ * Make your feature addition or bug fix.
41
+ * Add tests for it. This is important so I don't break it in a
42
+ future version unintentionally.
43
+ * Commit, do not mess with rakefile, version, or history.
44
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
45
+ * Send me a pull request. Bonus points for topic branches.
46
+
47
+ == Copyright
48
+
49
+ Copyright (c) 2010 Jeremy McAnally. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "wmata"
8
+ gem.summary = %Q{A gem for the WMATA API}
9
+ gem.description = %Q{A gem for accessing the WMATA API}
10
+ gem.email = "jeremymcanally@gmail.com"
11
+ gem.homepage = "http://github.com/jm/wmata"
12
+ gem.authors = ["Jeremy McAnally"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
18
+ end
19
+
20
+ require 'rake/testtask'
21
+ Rake::TestTask.new(:test) do |test|
22
+ test.libs << 'lib' << 'test'
23
+ test.pattern = 'test/**/test_*.rb'
24
+ test.verbose = true
25
+ end
26
+
27
+ begin
28
+ require 'rcov/rcovtask'
29
+ Rcov::RcovTask.new do |test|
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+ rescue LoadError
35
+ task :rcov do
36
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
37
+ end
38
+ end
39
+
40
+ task :test => :check_dependencies
41
+
42
+ task :default => :test
43
+
44
+ require 'rake/rdoctask'
45
+ Rake::RDocTask.new do |rdoc|
46
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "wmata #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
data/lib/resource.rb ADDED
@@ -0,0 +1,37 @@
1
+ module WMATA
2
+ class Resource
3
+ class <<self
4
+ def get_all(params={})
5
+ url = WMATA.base_url % [service, endpoint, to_query_string(params)]
6
+ HTTParty.get(url).first.last.map {|values| new(values) }
7
+ end
8
+
9
+ alias_method :find_all, :get_all
10
+
11
+ def service(value=nil)
12
+ @service = value if value
13
+ @service || "#{self.name.capitalize}s"
14
+ end
15
+
16
+ def endpoint(value=nil)
17
+ @endpoint = value if value
18
+ @endpoint || "#{self.name.capitalize}s"
19
+ end
20
+
21
+ def to_query_string(params)
22
+ "&" + params.map {|k, v| "#{k.to_s}=#{v}"}.join("&")
23
+ end
24
+ end
25
+
26
+ attr_reader :attrs
27
+
28
+ def initialize(attrs={})
29
+ @attrs = attrs
30
+ end
31
+
32
+ def method_missing(m, *args)
33
+ camel_cased = m.to_s.gsub(/\/(.?)/) { "::#{$1.upcase}" }.gsub(/(?:^|_)(.)/) { $1.upcase }
34
+ @attrs[m.to_s] or @attrs[camel_cased] or super
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ module WMATA
2
+ # A class representing service incidents in elevators (e.g., an elevator is busted
3
+ # going between the two floors of the station).
4
+ #
5
+ # Available attribute methods:
6
+ #
7
+ # * +display_order+ - Display priority
8
+ # * +date_out_of_service+ - Date when elevator/escalator was switched off.
9
+ # * +date_updated+ - Time when the information was last received.
10
+ # * +location_description+ - Location of elevator/escalator.
11
+ # * +station_code+ - Code of the station affected by the escalator/elevator incident.
12
+ # * +station_name+ - Name of the station affected by the escalator/elevator incident.
13
+ # * +symptom_code+ - ID of the reason why elevator/escalator was switched off.
14
+ # * +symptom_description+ - Information why elevator/escalator was switched off.
15
+ # * +time_out_of_service+ - Number of minutes the elevator has been out of service until last update of data.
16
+ # * +unit_name+ - ID of the affected elevator/escalator.
17
+ # * +unit_status+ - Can be "C" or "O": O means Out of service (has open issues) and C means Operational (open issues were closed).
18
+ # * +unit_type+ - "ESCALATOR" or "ELEVATOR"
19
+ #
20
+ class ElevatorIncident < Resource
21
+ service "Incidents"
22
+ endpoint "ElevatorIncidents"
23
+
24
+ # Get the incidents by station; provide either a +Station+ instance
25
+ # or a station code as the argument.
26
+ def self.get_by_station(affected_station)
27
+ @incidents ||= get_all
28
+ @incidents.select {|i| i.station_code == affected_station.to_s }.pop
29
+ end
30
+
31
+ # Get the station affected by the problem.
32
+ def affected_station
33
+ Station.get(@attrs['StationCode'])
34
+ end
35
+
36
+ alias_method :station, :affected_station
37
+
38
+ # Get a +Time+ object representing the time the elevator went out of
39
+ # service.
40
+ def date_out_of_service
41
+ Time.parse(@attrs['DateOutOfServ'])
42
+ end
43
+
44
+ # Get a +Time+ object representing the last time this API data entry
45
+ # was updated.
46
+ def date_updated
47
+ Time.parse(@attrs['DateUpdated'])
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,86 @@
1
+ module WMATA
2
+ # A resource class representing a rail line.
3
+ #
4
+ # Available attribute methods:
5
+ #
6
+ # * +display_name+ - The public name (color) of the line.
7
+ # * +start_station_code+ - The code associated with the first station on the line.
8
+ # * +end_station_code+ - The code associated with the last station on the line.
9
+ # * +internal_destination1+ - Some trains can start/finish their trips not only at the first/last station, but at intermediate stations along the line.
10
+ # * +internal_destination2+ - See +internal_destination+.
11
+ #
12
+ class Line < Resource
13
+ service "Rail"
14
+ endpoint "JLines"
15
+
16
+ SYMBOL_TO_LINES_MAP = {
17
+ :red => "RD",
18
+ :blue => "BL",
19
+ :orange => "OR",
20
+ :green => "GR",
21
+ :yellow => "YE"
22
+ }
23
+
24
+ class <<self
25
+ alias_method :get_all_without_memoize, :get_all
26
+
27
+ # NOTE: We memoize this since (a) there's no way to ask for just one line and
28
+ # (b) they're unlikely to change while we're doing a request.
29
+ def get_all(params)
30
+ @lines ||= get_all_without_memoize(params)
31
+ end
32
+
33
+ def symbol_to_line_code(symbol)
34
+ SYMBOL_TO_LINES_MAP[symbol]
35
+ end
36
+ end
37
+
38
+ # Get the first station on this line.
39
+ def start_station
40
+ @start_station ||= Station.get(@attrs['StartStationCode'])
41
+ end
42
+
43
+ # Get the last station on this line.
44
+ def end_station
45
+ @end_station ||= Station.get(@attrs['EndStationCode'])
46
+ end
47
+
48
+ # Get all internal destinations (some lines "end" or "begin" at more than
49
+ # one station).
50
+ def internal_destinations
51
+ [@attrs['InternalDestination1'], @attrs['InternalDestination2']].compact.map do |s|
52
+ Station.get(s)
53
+ end
54
+ end
55
+
56
+ # Get all rail incidents on this line.
57
+ def rail_incidents
58
+ @incidents ||= RailIncident.get_by_line(self)
59
+ end
60
+
61
+ alias_method :incidents, :rail_incidents
62
+
63
+ # Get all the stations on this line ordered by the route.
64
+ def route
65
+ Station.get_on_line(code)
66
+ end
67
+
68
+ alias_method :stations, :route
69
+
70
+ # Get a specific line, identified by line code (e.g., "RD") or a +Symbol+
71
+ # string name (e.g., +:red+).
72
+ def get(code)
73
+ code = Line.symbol_to_line_code(code) if code.is_a?(Symbol)
74
+ get_all.select {|l| l.code == code}.pop
75
+ end
76
+
77
+ # Returns the line's code (also available as +line_code+).
78
+ def code
79
+ @attrs['LineCode']
80
+ end
81
+
82
+ def to_s
83
+ code
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,37 @@
1
+ module WMATA
2
+ # A resource class representing a segment in a path between two stations.
3
+ #
4
+ # Available attribute methods:
5
+ #
6
+ # * +station_code+ - The ID code for an individual station.
7
+ # * +station_name+ - The name of the Station.
8
+ # * +line_code+ - The ID (color) of the Line associated with the path.
9
+ # * +seq_num+ - The sequence of the station in the path.
10
+ # * +distance_to_previous+ - Distance in feet from the previous station in the path.
11
+ #
12
+ class PathSegment < Resource
13
+ service "Rail"
14
+ endpoint "JPath"
15
+
16
+ # Return the +Station+ instance representing the station on
17
+ # this segment of the path.
18
+ def station
19
+ @station ||= Station.get(@attrs['StationCode'])
20
+ end
21
+
22
+ # Returns the +Line+ instance for the line this segment falls on.
23
+ def line
24
+ @line ||= Line.get(@attrs['LineCode'])
25
+ end
26
+
27
+ # The position this +PathSegment+ is in the overall path.
28
+ def index
29
+ @attrs['SeqNum']
30
+ end
31
+
32
+ # The distance to the previous station in the path.
33
+ def distance_to_previous
34
+ @attrs['DistanceToPrev']
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,59 @@
1
+ module WMATA
2
+ # A resource class representing train arrival prediction information.
3
+ #
4
+ # Available attribute methods:
5
+ #
6
+ # * +car+ - Number of cars in a particular train (usually 6 or 8).
7
+ # * +destination_code+ - The ID of destination station.
8
+ # * +destination_name+ - The name of destination station.
9
+ # * +group+ - Track number (1 or 2).
10
+ # * +line+ - ID of the metro line.
11
+ # * +location_code+ - ID of the station where the train is arriving.
12
+ # * +location_name+ - The name of the station where the train is arriving.
13
+ # * +arrival_status+ - The minutes to train arrival. Can be +:boarding+, +:arrived+, or positive number.
14
+ #
15
+ class Prediction < Resource
16
+ service "StationPrediction"
17
+
18
+ # Get train arrival prediction information for a given station; can
19
+ # be a station code as a string or a +Station+ instance.
20
+ def self.predict_for(station_code)
21
+ url = WMATA.base_url % [service, "GetPrediction/#{station_code.to_s}", ""]
22
+ HTTParty.get(url).first.last.map {|values| new(values) }
23
+ end
24
+
25
+ # Get the arriving station this prediction applies to.
26
+ def location
27
+ @location ||= Station.get(@attrs['LocationCode'])
28
+ end
29
+
30
+ alias_method :station, :location
31
+
32
+ # Get the destination of the train for this prediction.
33
+ def destination
34
+ @destination ||= Station.get(@attrs['DestinationCode'])
35
+ end
36
+
37
+ # Get the line code the line this prediction's station is on.
38
+ def line_code
39
+ @attrs['Line']
40
+ end
41
+
42
+ # Get the +Line+ instance for this prediction's station's line.
43
+ def line
44
+ @line ||= Line.get(@attrs['Line'])
45
+ end
46
+
47
+ # Get the arrival status of the train. Can be +:boarding+, +:arrived+, or
48
+ # the number of minutes until the train will arrive.
49
+ def arrival_status
50
+ if @attrs['Min'] == "BRD"
51
+ :boarding
52
+ elsif @attrs['Min'] == "ARR"
53
+ :arrived
54
+ else
55
+ @attrs['Min'].to_i
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,52 @@
1
+ module WMATA
2
+ # A resource class representing a rail incident (e.g., garbage on the rails delaying a train).
3
+ #
4
+ # Available attribute methods:
5
+ #
6
+ # * +incident_id+ - ID of the nicident
7
+ # * +incident_type+ - Type of the nicident
8
+ # * +date_updated+ - Date and time where information was updated.
9
+ # * +delay_severity+ - Severity of delay (if any). Can be +:minor+, +:major+, or +:medium+.
10
+ # * +description+ - Description what happened.
11
+ # * +emergency_text+ - Some text for emergency (if any).
12
+ # * +start_location_full_name+ - Station where delay starts.
13
+ # * +end_location_full_name+ - Station where delay ends.
14
+ # * +passenger_delay+ - Delay in minutes.
15
+ #
16
+ class RailIncident < Resource
17
+ service "Incidents"
18
+ endpoint "Incidents"
19
+
20
+ # Get all rail incidents by the line; can be a line code string or
21
+ # a +Line+ instance.
22
+ def self.get_by_line(line)
23
+ @incidents ||= get_all
24
+ @incidents.select {|i| i.line_codes_affected.include?(line.to_s)}
25
+ end
26
+
27
+ # Get a +Time+ object representing the last time this API data entry
28
+ # was updated.
29
+ def date_updated
30
+ Time.parse(@attrs['DateUpdated'])
31
+ end
32
+
33
+ # Get an array of the line codes affected by this incident.
34
+ def line_codes_affected
35
+ @attrs['LinesAffected'].split(";").reject {|s| s.empty? || s.nil?}
36
+ end
37
+
38
+ # Get +Line+ instances for the lines affected by this incident.
39
+ def lines_affected
40
+ @lines_affected = line_codes_affected.map {|l| Line.get(l.strip)}
41
+ end
42
+
43
+ # ID of the incident.
44
+ def incident_id
45
+ @attrs['IncidentID']
46
+ end
47
+
48
+ def delay_severity
49
+ @attrs['DelaySeverity'].to_s.downcase.to_sym
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,85 @@
1
+ module WMATA
2
+ # Resource class representing a station in the metro system.
3
+ #
4
+ # Available attribute methods:
5
+ #
6
+ # * +code - The code associated with a specific station.
7
+ # * +name - The name of the station.
8
+ # * +lat+ - The latitude of the station.
9
+ # * +lon+ - The longitude of the station.
10
+ #
11
+ # StationTogether2 - Unused.
12
+ class Station < Resource
13
+ service "Rail"
14
+ endpoint "JStations"
15
+
16
+ # Get all stations on a given line; argument can be a +Line+ instance, a string
17
+ # line code, or a symbol name (e.g., +:red+).
18
+ def self.get_on_line(line)
19
+ line = Line.symbol_to_line_code(line) if line.is_a?(Symbol)
20
+ get_all("LineCode" => line.to_s)
21
+ end
22
+
23
+ # Get a specific station by its code.
24
+ def self.get(code)
25
+ url = WMATA.base_url % [service, "JStationInfo", to_query_string("StationCode" => code)]
26
+ new(HTTParty.get(url))
27
+ end
28
+
29
+ # Get all possible codes for this station (some stations are in together with another so
30
+ # they are technically identified by two station codes).
31
+ def codes
32
+ [@attrs['Code'], @attrs['StationTogether1'], @attrs['StationTogether2']].compact
33
+ end
34
+
35
+ # Get the line codes for this station (some stations serve more than one line).
36
+ def line_codes
37
+ [@attrs['LineCode1'], @attrs['LineCode2'], @attrs['LineCode3'], @attrs['LineCode4']].compact
38
+ end
39
+
40
+ # Get +Line+ instances for the lines serviced by this station.
41
+ def lines
42
+ @lines ||= line_codes.map {|l| Line.get(l)}
43
+ end
44
+
45
+ # Get train arrival predictions for this station.
46
+ def predictions
47
+ @predictions ||= Prediction.predict_for(self)
48
+ end
49
+
50
+ # Get all elevator incidents affecting this station.
51
+ def elevator_incidents
52
+ @elevator_incidents ||= ElevatorIncident.get_by_station(self)
53
+ end
54
+
55
+ # Build a path from this station to another identified by its code or as a
56
+ # +Station+ instance.
57
+ def path_to(to)
58
+ WMATA.build_path(self, to)
59
+ end
60
+
61
+ # Build a path from this station to another station identified by its code
62
+ # or as a +Station+ instance.
63
+ def path_from(from)
64
+ WMATA.build_path(from, self)
65
+ end
66
+
67
+ def latitude
68
+ @attrs['Lat']
69
+ end
70
+
71
+ def longitude
72
+ @attrs['Lon']
73
+ end
74
+
75
+ def coordinates
76
+ [latitude, longitude]
77
+ end
78
+
79
+ alias_method :coords, :coordinates
80
+
81
+ def to_s
82
+ @attrs['Code']
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,40 @@
1
+ module WMATA
2
+ # A resource class representing a station entrance (even if it's just an elevator).
3
+ #
4
+ # Available attribute methods:
5
+ #
6
+ # * +id+ - ID of the entrance.
7
+ # * +name+ - The name of the entrance.
8
+ # * +description+ - A description of the entrance.
9
+ # * +lat+ - The entrance's latitude.
10
+ # * +lon+ - The entrance's longitude.
11
+ #
12
+ class StationEntrance < Resource
13
+ service "Rail"
14
+ endpoint "JStationEntrances"
15
+
16
+ # Get station codes that this entrance serves.
17
+ def station_codes
18
+ [@attrs['StationCode1'], @attrs['StationCode2']].compact
19
+ end
20
+
21
+ # Get the +Station+ instance for this entrance.
22
+ def station
23
+ @station ||= Station.get(station_codes.first)
24
+ end
25
+
26
+ def latitude
27
+ @attrs['Lat']
28
+ end
29
+
30
+ def longitude
31
+ @attrs['Lon']
32
+ end
33
+
34
+ def coordinates
35
+ [latitude, longitude]
36
+ end
37
+
38
+ alias_method :coords, :coordinates
39
+ end
40
+ end
data/lib/wmata.rb ADDED
@@ -0,0 +1,128 @@
1
+ $:.unshift(File.dirname(__FILE__))
2
+
3
+ require 'rubygems'
4
+ require 'httparty'
5
+ require 'forwardable'
6
+
7
+ require 'resource'
8
+ require 'resources/line'
9
+ require 'resources/station'
10
+ require 'resources/rail_incident'
11
+ require 'resources/elevator_incident'
12
+ require 'resources/prediction'
13
+ require 'resources/path_segment'
14
+ require 'resources/station_entrance'
15
+
16
+ module WMATA
17
+ BASE_URL = "http://api.wmata.com/%s.svc/json/%s?api_key=%s%s"
18
+
19
+ class <<self
20
+ attr_accessor :api_key
21
+
22
+ # Get the base URL based on the API key given. Used in
23
+ # nearly every method that contacts the remote API.
24
+ def base_url
25
+ BASE_URL.dup % ["%s", "%s", @api_key, "%s"]
26
+ end
27
+
28
+ # Get all rail lines.
29
+ #
30
+ # WMATA.lines.map {|l| l.code}
31
+ # # => ["RD", "BL", "GR", "OR", "YE"]
32
+ #
33
+ def lines
34
+ Line.get_all
35
+ end
36
+
37
+ # Get all stations.
38
+ #
39
+ # WMATA.stations.map {|s| s.name }
40
+ # # => ["McPherson Square", "Metro Center", ...]
41
+ #
42
+ def stations
43
+ Station.get_all
44
+ end
45
+
46
+ # Get a specific station by code.
47
+ #
48
+ # WMATA.station("C02")
49
+ # # => #<Station:0x1205aee8 "McPherson Square">
50
+ #
51
+ def station(code)
52
+ Station.get(code)
53
+ end
54
+
55
+ # Get an array of stations on a particular line. Can be called
56
+ # with a line code (e.g., RD) or a symbol for the line name
57
+ # (e.g., +:red+).
58
+ #
59
+ # WMATA.stations_on_line(:red)
60
+ # # => [#<Station:0x0702aca8>, ...]
61
+ #
62
+ def stations_on_line(code)
63
+ Station.get_on_line(code)
64
+ end
65
+
66
+ # Get station predictions (i.e., train arrival information seen on
67
+ # station terminals) for a specific station; if no station
68
+ # code is provided, it will get predictions for all stations.
69
+ #
70
+ # predictions = WMATA.predict_for("C02")
71
+ # # => [#<Prediction:0x1205aee8 ...>, ...]
72
+ # puts "#{predictions.first.location_name} => #{predictions.first.destination_name}"
73
+ # # McPherson Square => Metro Center
74
+ #
75
+ def predict_for(station="All")
76
+ Prediction.predict_for(station)
77
+ end
78
+
79
+ alias_method :get_predictions, :predict_for
80
+
81
+ # Get an array of rail incidents for all lines; use the +incidents+ method
82
+ # on +Line+ to get them for a specific line or the same method.
83
+ #
84
+ # WMATA.rail_incidents.map {|i| i.description}
85
+ # # => ["Friendship Heights is closed...", ...]
86
+ #
87
+ def rail_incidents
88
+ RailIncident.get_all
89
+ end
90
+
91
+ alias_method :incidents, :rail_incidents
92
+
93
+ # Get an array of elevator incidents for all lines; use the +elevator_incidents+
94
+ # method on +Station+ to get them for a specific station.
95
+ #
96
+ # WMATA.elevator_incidents.map {|i| i.symptom_code}
97
+ # # => ["1419", ...]
98
+ #
99
+ def elevator_incidents
100
+ ElevatorIncident.get_all
101
+ end
102
+
103
+ # Map a path between two stations, identified by their station codes; returns an
104
+ # array of stations ordered by the path. You can also provide +Station+ instances
105
+ # and get a path between them.
106
+ #
107
+ # WMATA.build_path("C02", "A01")
108
+ # # => [#<Station:0x0702aca8>, ...]
109
+ #
110
+ def build_path(from, to)
111
+ PathSegment.get_all("FromStationCode" => from, "ToStationCode" => to)
112
+ end
113
+
114
+ # Find entrances near a given latitude and longitude within a given radius (in meters).
115
+ # If no geolocation information is given, all entrances are returned.
116
+ #
117
+ # WMATA.entrances(:lat => 28.82083, :lon => 88.9239423, :radius => 2000)
118
+ # # => [#<StationEntrance:0x0702aca8>, ...]
119
+ #
120
+ def entrances(from={})
121
+ params = {:lat => 0, :lon => 0, :radius => 500}.merge(from)
122
+ StationEntrance.get_all(params)
123
+ end
124
+
125
+ alias_method :station_entrances, :entrances
126
+ alias_method :entrances_near, :entrances
127
+ end
128
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,14 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'flexmock'
4
+ require 'flexmock/test_unit'
5
+ require 'ostruct'
6
+
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
8
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
9
+
10
+ require 'wmata'
11
+
12
+ class Test::Unit::TestCase
13
+
14
+ end
@@ -0,0 +1,28 @@
1
+ require 'helper'
2
+
3
+ class TestElevatorIncident < Test::Unit::TestCase
4
+ def test_get_by_station
5
+ fake_incidents = []
6
+ 5.times {|i| fake_incidents << OpenStruct.new(:station_code => "A#{i}")}
7
+ flexmock(WMATA::ElevatorIncident).should_receive(:get_all).and_return(fake_incidents)
8
+
9
+ assert_equal "A2", WMATA::ElevatorIncident.get_by_station("A2").station_code
10
+ end
11
+
12
+ def test_affected_station
13
+ flexmock(WMATA::Station).should_receive(:get).and_return(OpenStruct.new(:name => "Winner"))
14
+ incident = WMATA::ElevatorIncident.new("StationCode" => "A4")
15
+
16
+ assert_equal "Winner", incident.affected_station.name
17
+ end
18
+
19
+ def test_date_out_of_service
20
+ incident = WMATA::ElevatorIncident.new("DateOutOfServ" => "2010-07-27T00:00:00")
21
+ assert_equal Time.parse("07/27/2010"), incident.date_out_of_service
22
+ end
23
+
24
+ def test_date_updated
25
+ incident = WMATA::ElevatorIncident.new("DateUpdated" => "2010-07-27T00:00:00")
26
+ assert_equal Time.parse("07/27/2010"), incident.date_updated
27
+ end
28
+ end
data/test/test_line.rb ADDED
@@ -0,0 +1,61 @@
1
+ class TestLine < Test::Unit::TestCase
2
+ def test_start_station
3
+ flexmock(WMATA::Station).should_receive(:get).and_return(OpenStruct.new(:name => "Winner"))
4
+ line = WMATA::Line.new("StartStationCode" => "A4")
5
+
6
+ assert_equal "Winner", line.start_station.name
7
+ end
8
+
9
+ def test_end_station
10
+ flexmock(WMATA::Station).should_receive(:get).and_return(OpenStruct.new(:name => "Winner"))
11
+ line = WMATA::Line.new("EndStationCode" => "A4")
12
+
13
+ assert_equal "Winner", line.end_station.name
14
+ end
15
+
16
+ def test_internal_destinations
17
+ flexmock(WMATA::Station).should_receive(:get).with("A1").and_return(OpenStruct.new(:name => "Winner"))
18
+ flexmock(WMATA::Station).should_receive(:get).with("A2").and_return(OpenStruct.new(:name => "Failure"))
19
+ line = WMATA::Line.new("InternalDestination1" => "A1", "InternalDestination2" => "A2")
20
+
21
+ assert_equal ["Failure", "Winner"], line.internal_destinations.map {|d| d.name}.sort
22
+ end
23
+
24
+ def test_internal_destinations_with_partial_set
25
+ flexmock(WMATA::Station).should_receive(:get).with("A1").and_return(OpenStruct.new(:name => "Winner"))
26
+ line = WMATA::Line.new("InternalDestination1" => "A1")
27
+
28
+ assert_equal ["Winner"], line.internal_destinations.map {|d| d.name}.sort
29
+ end
30
+
31
+ def test_rail_incidents
32
+ flexmock(WMATA::RailIncident).should_receive(:get_by_line).and_return([OpenStruct.new(:name => "Winner")])
33
+ line = WMATA::Line.new("LineCode" => "RD")
34
+
35
+ assert_equal ["Winner"], line.rail_incidents.map(&:name)
36
+ end
37
+
38
+ def test_route
39
+ flexmock(WMATA::Station).should_receive(:get_on_line).and_return([OpenStruct.new(:name => "Winner")])
40
+ line = WMATA::Line.new("LineCode" => "RD")
41
+
42
+ assert_equal ["Winner"], line.route.map(&:name)
43
+ end
44
+
45
+ def test_get
46
+ flexmock(WMATA::Station).should_receive(:get_on_line).and_return([OpenStruct.new(:name => "Winner")])
47
+ line = WMATA::Line.new("LineCode" => "RD")
48
+
49
+ assert_equal ["Winner"], line.route.map(&:name)
50
+ end
51
+
52
+ def test_code
53
+ line = WMATA::Line.new("LineCode" => "RD")
54
+ assert_equal "RD", line.code
55
+ end
56
+
57
+ def test_to_s
58
+ line = WMATA::Line.new("LineCode" => "RD")
59
+ assert_equal "RD", line.to_s
60
+ end
61
+ end
@@ -0,0 +1,22 @@
1
+ require 'helper'
2
+
3
+ class TestPathSegment < Test::Unit::TestCase
4
+ def test_station
5
+ flexmock(WMATA::Station).should_receive(:get).and_return(OpenStruct.new(:name => "Winner"))
6
+ segment = WMATA::PathSegment.new("StationCode" => "A4")
7
+
8
+ assert_equal "Winner", segment.station.name
9
+ end
10
+
11
+ def test_line
12
+ flexmock(WMATA::Line).should_receive(:get).and_return(OpenStruct.new(:name => "Winner"))
13
+ segment = WMATA::PathSegment.new("LineCode" => "A4")
14
+
15
+ assert_equal "Winner", segment.line.name
16
+ end
17
+
18
+ def test_index
19
+ segment = WMATA::PathSegment.new("SeqNum" => "1")
20
+ assert_equal "1", segment.index
21
+ end
22
+ end
@@ -0,0 +1,36 @@
1
+ require 'helper'
2
+
3
+ class TestPrediction< Test::Unit::TestCase
4
+ def setup
5
+ WMATA.api_key = "1234"
6
+ @fake = OpenStruct.new(:name => "Winner")
7
+ @prediction = WMATA::Prediction.new("LocationCode" => "A4", "DestinationCode" => "A3", "Line" => "RD")
8
+ end
9
+
10
+ def test_predict_for
11
+ flexmock(HTTParty).should_receive(:get).with("http://api.wmata.com/StationPrediction.svc/json/GetPrediction/A4?api_key=1234").and_return([[{"Things" => 1234}]])
12
+ prediction = WMATA::Prediction.predict_for("A4")
13
+ end
14
+
15
+ def test_location
16
+ flexmock(WMATA::Station).should_receive(:get).and_return(@fake)
17
+
18
+ assert_equal @fake, @prediction.location
19
+ end
20
+
21
+ def test_destination
22
+ flexmock(WMATA::Station).should_receive(:get).and_return(@fake)
23
+
24
+ assert_equal @fake, @prediction.destination
25
+ end
26
+
27
+ def test_line_code
28
+ assert_equal "RD", @prediction.line_code
29
+ end
30
+
31
+ def test_line
32
+ flexmock(WMATA::Line).should_receive(:get).and_return(@fake)
33
+
34
+ assert_equal @fake, @prediction.line
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ require 'helper'
2
+
3
+ class TestRailIncident < Test::Unit::TestCase
4
+ def test_get_by_station
5
+ fake_incidents = []
6
+ 5.times {|i| fake_incidents << OpenStruct.new(:line_codes_affected => ["RD", "BL#{i}"])}
7
+ flexmock(WMATA::RailIncident).should_receive(:get_all).and_return(fake_incidents)
8
+
9
+ assert_equal ["BL2", "RD"], WMATA::RailIncident.get_by_line("RD")[2].line_codes_affected.sort
10
+ end
11
+
12
+ def test_line_codes_affected
13
+ incident = WMATA::RailIncident.new("LinesAffected" => "RD;BL;OR")
14
+ assert_equal ["BL", "OR", "RD"], incident.line_codes_affected.sort
15
+ end
16
+
17
+ def test_lines_affected
18
+ flexmock(WMATA::Line).should_receive(:get).and_return(WMATA::Line.new("Code" => "RD"))
19
+ incident = WMATA::RailIncident.new("LinesAffected" => "RD;BL;OR")
20
+
21
+ assert_equal 3, incident.lines_affected.length
22
+ end
23
+
24
+ def test_date_updated
25
+ incident = WMATA::RailIncident.new("DateUpdated" => "2010-07-27T00:00:00")
26
+ assert_equal Time.parse("07/27/2010"), incident.date_updated
27
+ end
28
+ end
@@ -0,0 +1,42 @@
1
+ require 'helper'
2
+
3
+ class TestResource < Test::Unit::TestCase
4
+ class Faker < WMATA::Resource
5
+ end
6
+
7
+ def setup
8
+ @fake = Faker.new("method1" => "win", "MethodTwo" => "epic")
9
+ end
10
+
11
+ def test_method_mapping
12
+ assert_nothing_raised do
13
+ assert_equal "win", @fake.method1
14
+ end
15
+ end
16
+
17
+ def test_method_mapping_with_camel_case
18
+ assert_nothing_raised do
19
+ assert_equal "epic", @fake.method_two
20
+ end
21
+ end
22
+
23
+ def test_method_missing_fails_if_missing
24
+ assert_raises(NoMethodError) do
25
+ @fake.whateva
26
+ end
27
+ end
28
+
29
+ def test_service_set
30
+ Faker.service "Rail"
31
+ assert_equal "Rail", Faker.service
32
+ end
33
+
34
+ def test_endpoint_set
35
+ Faker.endpoint "Rail"
36
+ assert_equal "Rail", Faker.endpoint
37
+ end
38
+
39
+ def test_to_query_string
40
+ assert_equal "&things=yes&whatever=no", Faker.to_query_string(:things => "yes", :whatever => "no")
41
+ end
42
+ end
@@ -0,0 +1,66 @@
1
+ require 'helper'
2
+
3
+ class TestStation < Test::Unit::TestCase
4
+ def test_get_on_line
5
+ flexmock(WMATA::Station).should_receive(:get_all).with("LineCode" => "RD")
6
+ WMATA::Station.get_on_line("RD")
7
+ end
8
+
9
+ def test_get_on_line_with_symbol
10
+ flexmock(WMATA::Station).should_receive(:get_all).with("LineCode" => "RD")
11
+ WMATA::Station.get_on_line(:red)
12
+ end
13
+
14
+ def test_get
15
+ flexmock(HTTParty).should_receive(:get).with("http://api.wmata.com/Rail.svc/json/JStationInfo?api_key=1234&StationCode=A4").and_return({"Things" => "1234"})
16
+ station = WMATA::Station.get("A4")
17
+
18
+ assert_equal "1234", station.things
19
+ end
20
+
21
+ def test_codes
22
+ station = WMATA::Station.new("Code" => "A1", "StationTogether1" => "C2", "StationTogether2" => "D1")
23
+ assert_equal ["A1", "C2", "D1"], station.codes.sort
24
+ end
25
+
26
+ def test_codes_with_partial_set
27
+ station = WMATA::Station.new("Code" => "A1", "StationTogether2" => "D1")
28
+ assert_equal ["A1", "D1"], station.codes.sort
29
+ end
30
+
31
+ def test_line_codes
32
+ station = WMATA::Station.new("LineCode1" => "RD", "LineCode2" => "BL", "LineCode3" => "GR", "LineCode4" => "OR")
33
+ assert_equal ["BL", "GR", "OR", "RD"], station.line_codes.sort
34
+ end
35
+
36
+ def test_line_codes_with_partial_set
37
+ station = WMATA::Station.new("LineCode1" => "RD", "LineCode3" => "GR", "LineCode4" => "OR")
38
+ assert_equal ["GR", "OR", "RD"], station.line_codes.sort
39
+ end
40
+
41
+ def test_lines
42
+ flexmock(WMATA::Line).should_receive(:get).with("RD").and_return(OpenStruct.new("code" => "RD"))
43
+ station = WMATA::Station.new("LineCode1" => "RD")
44
+
45
+ assert_equal "RD", station.lines.first.code
46
+ end
47
+
48
+ def test_predictions
49
+ flexmock(WMATA::Prediction).should_receive(:predict_for).and_return([])
50
+ station = WMATA::Station.new("Code" => "A1")
51
+
52
+ assert_equal [], station.predictions
53
+ end
54
+
55
+ def test_elevator_incidents
56
+ flexmock(WMATA::ElevatorIncident).should_receive(:get_by_station).and_return([])
57
+ station = WMATA::Station.new("Code" => "A1")
58
+
59
+ assert_equal [], station.elevator_incidents
60
+ end
61
+
62
+ def test_to_s
63
+ station = WMATA::Station.new("Code" => "A1")
64
+ assert_equal "A1", station.to_s
65
+ end
66
+ end
@@ -0,0 +1,13 @@
1
+ require 'helper'
2
+
3
+ class TestStationEntrance < Test::Unit::TestCase
4
+ def test_station_codes
5
+ station = WMATA::StationEntrance.new("StationCode1" => "A1", "StationCode2" => "A2")
6
+ assert_equal ["A1","A2"], station.station_codes.sort
7
+ end
8
+
9
+ def test_station_codes_with_partial_set
10
+ station = WMATA::StationEntrance.new("StationCode2" => "A2")
11
+ assert_equal ["A2"], station.station_codes.sort
12
+ end
13
+ end
@@ -0,0 +1,66 @@
1
+ require 'helper'
2
+
3
+ class TestWmata < Test::Unit::TestCase
4
+ def setup
5
+ flexmock(WMATA).should_receive(:api_key).and_return("1234")
6
+ end
7
+
8
+ def test_lines
9
+ mock_resource(WMATA::Line)
10
+ WMATA.lines
11
+ end
12
+
13
+ def test_stations
14
+ mock_resource(WMATA::Station)
15
+ WMATA.stations
16
+ end
17
+
18
+ def test_station
19
+ flexmock(WMATA::Station).should_receive(:get).with("A2")
20
+ WMATA.station("A2")
21
+ end
22
+
23
+ def test_stations_on_line
24
+ flexmock(WMATA::Station).should_receive(:get_on_line).with(:red)
25
+ WMATA.stations_on_line(:red)
26
+ end
27
+
28
+ def test_predict_for
29
+ flexmock(WMATA::Prediction).should_receive(:predict_for).with("A7")
30
+ WMATA.predict_for("A7")
31
+ end
32
+
33
+ def test_predict_for_with_default
34
+ flexmock(WMATA::Prediction).should_receive(:predict_for).with("All")
35
+ WMATA.predict_for
36
+ end
37
+
38
+ def test_rail_incidents
39
+ mock_resource(WMATA::RailIncident)
40
+ WMATA.rail_incidents
41
+ end
42
+
43
+ def test_elevator_incidents
44
+ mock_resource(WMATA::ElevatorIncident)
45
+ WMATA.elevator_incidents
46
+ end
47
+
48
+ def test_build_path
49
+ flexmock(WMATA::PathSegment).should_receive(:get_all).with("FromStationCode" => "37.80", "ToStationCode" => "88.7")
50
+ WMATA.build_path("37.80", "88.7")
51
+ end
52
+
53
+ def test_entrances
54
+ flexmock(WMATA::StationEntrance).should_receive(:get_all).with(:lat => 0, :lon => 0, :radius => 500)
55
+ WMATA.entrances
56
+ end
57
+
58
+ def test_entrances_with_specifics
59
+ flexmock(WMATA::StationEntrance).should_receive(:get_all).with(:lat => 39.0, :lon => 0, :radius => 2000)
60
+ WMATA.entrances(:lat => 39.0, :radius => 2000)
61
+ end
62
+
63
+ def mock_resource(resource_class)
64
+ flexmock(resource_class).should_receive(:get_all)
65
+ end
66
+ end
data/wmata.gemspec ADDED
@@ -0,0 +1,75 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{pius-wmata-hotfix}
8
+ s.version = "0.2.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Jeremy McAnally"]
12
+ s.date = %q{2010-08-17}
13
+ s.description = %q{A gem for accessing the WMATA API}
14
+ s.email = %q{jeremymcanally@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/resource.rb",
27
+ "lib/resources/elevator_incident.rb",
28
+ "lib/resources/line.rb",
29
+ "lib/resources/path_segment.rb",
30
+ "lib/resources/prediction.rb",
31
+ "lib/resources/rail_incident.rb",
32
+ "lib/resources/station.rb",
33
+ "lib/resources/station_entrance.rb",
34
+ "lib/wmata.rb",
35
+ "test/helper.rb",
36
+ "test/test_elevator_incident.rb",
37
+ "test/test_line.rb",
38
+ "test/test_path_segment.rb",
39
+ "test/test_prediction.rb",
40
+ "test/test_rail_incident.rb",
41
+ "test/test_resource.rb",
42
+ "test/test_station.rb",
43
+ "test/test_station_entrance.rb",
44
+ "test/test_wmata.rb",
45
+ "wmata.gemspec"
46
+ ]
47
+ s.homepage = %q{http://github.com/jm/wmata}
48
+ s.rdoc_options = ["--charset=UTF-8"]
49
+ s.require_paths = ["lib"]
50
+ s.rubygems_version = %q{1.3.6}
51
+ s.summary = %q{A gem for the WMATA API}
52
+ s.test_files = [
53
+ "test/helper.rb",
54
+ "test/test_elevator_incident.rb",
55
+ "test/test_line.rb",
56
+ "test/test_path_segment.rb",
57
+ "test/test_prediction.rb",
58
+ "test/test_rail_incident.rb",
59
+ "test/test_resource.rb",
60
+ "test/test_station.rb",
61
+ "test/test_station_entrance.rb",
62
+ "test/test_wmata.rb"
63
+ ]
64
+
65
+ if s.respond_to? :specification_version then
66
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
67
+ s.specification_version = 3
68
+
69
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
70
+ else
71
+ end
72
+ else
73
+ end
74
+ end
75
+
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pius-wmata-hotfix
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
+ platform: ruby
11
+ authors:
12
+ - Jeremy McAnally
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-08-17 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: A gem for accessing the WMATA API
22
+ email: jeremymcanally@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - LICENSE
29
+ - README.rdoc
30
+ files:
31
+ - .document
32
+ - .gitignore
33
+ - LICENSE
34
+ - README.rdoc
35
+ - Rakefile
36
+ - VERSION
37
+ - lib/resource.rb
38
+ - lib/resources/elevator_incident.rb
39
+ - lib/resources/line.rb
40
+ - lib/resources/path_segment.rb
41
+ - lib/resources/prediction.rb
42
+ - lib/resources/rail_incident.rb
43
+ - lib/resources/station.rb
44
+ - lib/resources/station_entrance.rb
45
+ - lib/wmata.rb
46
+ - test/helper.rb
47
+ - test/test_elevator_incident.rb
48
+ - test/test_line.rb
49
+ - test/test_path_segment.rb
50
+ - test/test_prediction.rb
51
+ - test/test_rail_incident.rb
52
+ - test/test_resource.rb
53
+ - test/test_station.rb
54
+ - test/test_station_entrance.rb
55
+ - test/test_wmata.rb
56
+ - wmata.gemspec
57
+ has_rdoc: true
58
+ homepage: http://github.com/pius/wmata
59
+ licenses: []
60
+
61
+ post_install_message:
62
+ rdoc_options:
63
+ - --charset=UTF-8
64
+ require_paths:
65
+ - lib
66
+ required_ruby_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ segments:
71
+ - 0
72
+ version: "0"
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ requirements: []
81
+
82
+ rubyforge_project:
83
+ rubygems_version: 1.3.6
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: A gem for the WMATA API
87
+ test_files:
88
+ - test/helper.rb
89
+ - test/test_elevator_incident.rb
90
+ - test/test_line.rb
91
+ - test/test_path_segment.rb
92
+ - test/test_prediction.rb
93
+ - test/test_rail_incident.rb
94
+ - test/test_resource.rb
95
+ - test/test_station.rb
96
+ - test/test_station_entrance.rb
97
+ - test/test_wmata.rb