gtfs_reader 1.0.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.
@@ -0,0 +1,94 @@
1
+ module GtfsReader
2
+ class BulkFeedHandler
3
+ def initialize(bulk_size, args=[], &block)
4
+ @bulk_size = bulk_size
5
+ @callbacks = {}
6
+ BulkFeedHandlerDsl.new(self).instance_exec *args, &block
7
+ end
8
+
9
+ def handler?(filename)
10
+ @callbacks.key? filename
11
+ end
12
+
13
+ def handle_file(filename, reader)
14
+ unless @callbacks.key? filename
15
+ Log.warn { "No handler registered for #{filename.to_s.red}.txt" }
16
+ return
17
+ end
18
+
19
+ calls = callbacks filename
20
+ calls[:before].call if calls.key? :before
21
+ read_row = calls[:read]
22
+
23
+ values = []
24
+ total = bulk_count = 0
25
+ cols = reader.col_names
26
+ reader.each do |row|
27
+ values << (read_row ? read_row.call(row) : row)
28
+ bulk_count += 1
29
+
30
+ if bulk_count == @bulk_size
31
+ total += bulk_count
32
+ calls[:bulk].call values, bulk_count, total, cols
33
+ bulk_count = 0
34
+ values = []
35
+ end
36
+ end
37
+
38
+ unless bulk_count == 0
39
+ calls[:bulk].call values, bulk_count, (total + bulk_count), cols
40
+ end
41
+ nil
42
+ end
43
+
44
+ def create_callback(kind, filename, block)
45
+ Log.debug do
46
+ kind_str = kind.to_s.center(:before.to_s.length).magenta
47
+ "create callback (#{kind_str}) #{filename.to_s.red}"
48
+ end
49
+ callbacks(filename)[kind] = block
50
+ end
51
+
52
+ def callback?(kind, filename)
53
+ @callbacks.key? filename and @callbacks[filename].key? kind
54
+ end
55
+
56
+ private
57
+
58
+ def callbacks(filename)
59
+ @callbacks[filename] ||= {}
60
+ end
61
+ end
62
+
63
+ class BulkFeedHandlerDsl
64
+ def initialize(feed_handler)
65
+ @feed_handler = feed_handler
66
+ end
67
+
68
+ def method_missing(filename, *args, &block)
69
+ BulkDsl.new(@feed_handler, filename).instance_exec &block
70
+
71
+ unless @feed_handler.callback? :bulk, filename
72
+ raise HandlerMissingError, "No bulk block for #{filename}"
73
+ end
74
+ end
75
+ end
76
+
77
+ class BulkDsl
78
+ def initialize(feed_handler, filename)
79
+ @feed_handler, @filename = feed_handler, filename
80
+ end
81
+
82
+ def before(&block)
83
+ @feed_handler.create_callback :before, @filename, block
84
+ end
85
+
86
+ def read(&block)
87
+ @feed_handler.create_callback :read, @filename, block
88
+ end
89
+
90
+ def bulk(&block)
91
+ @feed_handler.create_callback :bulk, @filename, block
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,60 @@
1
+ module GtfsReader
2
+ module Config
3
+ # Defines a single column in a {FileDefinition file}.
4
+ class Column
5
+ # A "parser" which simply returns its input. Used by default
6
+ IDENTITY_PARSER = ->(arg) { arg }
7
+
8
+ attr_reader :name
9
+
10
+ #@param name [String] the name of the column
11
+ #@option opts [Boolean] :required (false) If this column is required to
12
+ # appear in the given file
13
+ #@option opts [Boolean] :unique (false) if values in this column need to be
14
+ # unique among all rows in the file.
15
+ def initialize(name, opts={}, &parser)
16
+ @name = name
17
+ @parser = block_given? ? parser : IDENTITY_PARSER
18
+
19
+ @opts = { required: false, unique: false }.merge ( opts || {} )
20
+ end
21
+
22
+ def parser(&block)
23
+ @parser = block if block_given?
24
+ @parser
25
+ end
26
+
27
+ #@return [Boolean] if this column is required to appear in the file
28
+ def required?
29
+ @opts[:required]
30
+ end
31
+
32
+ #@return [Boolean] if values in this column need to be unique among all rows
33
+ # in the file.
34
+ def unique?
35
+ @opts[:unique]
36
+ end
37
+
38
+ #@return [Boolean]
39
+ def parser?
40
+ parser != IDENTITY_PARSER
41
+ end
42
+
43
+ def to_s
44
+ opts = @opts.map do |key,value|
45
+ case value
46
+ when true then key
47
+ when false,nil then nil
48
+ else "#{key}=#{value}"
49
+ end
50
+ end.reject &:nil?
51
+
52
+ opts << 'has_parser' if parser?
53
+
54
+ str = name.to_s
55
+ str << ": #{opts.join ', '}" unless opts.empty?
56
+ "[#{str}]"
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,190 @@
1
+ require_relative '../feed_definition'
2
+
3
+ # This default config file creates a [FeedDefinition] that matches the one
4
+ # specified by Google. You can use this definition in most cases. A custom
5
+ # definition will only be required if you need to parse a feed that differs in
6
+ # some critical way (Remember that these feeds are not always created by
7
+ # technically-proficient people).
8
+ # See https://developers.google.com/transit/gtfs/reference
9
+ module GtfsReader
10
+ module Config
11
+ module Defaults
12
+ FEED_DEFINITION = FeedDefinition.new.tap do |feed|
13
+ feed.instance_exec do
14
+ file :agency, required: true do
15
+ col :agency_name, required: true
16
+ col :agency_url, required: true
17
+ col :agency_timezone, required: true
18
+ col :agency_id, unique: true
19
+ col :agency_lang
20
+ col :agency_phone
21
+ col :agency_fare_url
22
+ end
23
+
24
+ file :stops, required: true do
25
+ col :stop_id, required: true, unique: true
26
+ col :stop_code
27
+ col :stop_name, required: true
28
+ col :stop_desc
29
+ col :stop_lat, required: true
30
+ col :stop_lon, required: true
31
+ col :stop_url
32
+ col :stop_timezone
33
+ col :zone_id
34
+ col :location_type, &output_map( :stop, station: ?1 )
35
+ col :parent_station
36
+
37
+ col :wheelchair_boarding do |val|
38
+ if parent_station
39
+ case val
40
+ when ?2 then :no
41
+ when ?1 then :yes
42
+ else :inherit
43
+ end
44
+ else
45
+ case val
46
+ when ?2 then :no_vehicles
47
+ when ?1 then :some_vehicles
48
+ else :unknown
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ file :routes, required: true do
55
+ col :route_id, required: true, unique: true
56
+ col :route_short_name, required: true
57
+ col :route_long_name, required: true
58
+ col :route_desc
59
+ col :route_type, required: true,
60
+ &output_map( :unknown,
61
+ tram: ?0,
62
+ subway: ?1,
63
+ rail: ?2,
64
+ bus: ?3,
65
+ ferry: ?4,
66
+ cable_car: ?5,
67
+ gondola: ?6,
68
+ funicular: ?7 )
69
+ col :route_url
70
+ col :route_color
71
+ col :route_text_color
72
+ col :agency_id
73
+ end
74
+
75
+ file :trips, required: true do
76
+ col :trip_id, required: true, unique: true
77
+ col :trip_headsign
78
+ col :trip_short_name
79
+ col :trip_long_name
80
+ col :route_id, required: true
81
+ col :service_id, required: true
82
+ col :direction_id, &output_map( primary: ?0, opposite: ?1 )
83
+ col :block_id
84
+ col :shape_id
85
+ col :wheelchair_accessible, &output_map( :unknown, yes: ?1, no: ?2 )
86
+ col :bikes_allowed, &output_map( :unknown, yes: ?1, no: ?2 )
87
+ end
88
+
89
+ file :stop_times, required: true do
90
+ col :trip_id, required: true
91
+ col :arrival_time, required: true
92
+ col :departure_time, required: true
93
+ col :stop_id, required: true
94
+ col :stop_sequence, required: true
95
+ col :stop_headsign
96
+
97
+ col :pickup_type,
98
+ &output_map( :regular,
99
+ none: ?1,
100
+ phone_agency: ?2,
101
+ coordinate_with_driver: ?3 )
102
+
103
+ col :drop_off_type,
104
+ &output_map( :regular,
105
+ none: ?1,
106
+ phone_agency: ?2,
107
+ coordinate_with_driver: ?3 )
108
+
109
+ col :shape_dist_traveled
110
+ end
111
+
112
+ file :calendar, required: true do
113
+ col :service_id, required: true, unique: true
114
+ col :monday, required: true, &output_map( yes: ?1, no: ?0 )
115
+ col :tuesday, required: true, &output_map( yes: ?1, no: ?0 )
116
+ col :wednesday, required: true, &output_map( yes: ?1, no: ?0 )
117
+ col :thursday, required: true, &output_map( yes: ?1, no: ?0 )
118
+ col :friday, required: true, &output_map( yes: ?1, no: ?0 )
119
+ col :saturday, required: true, &output_map( yes: ?1, no: ?0 )
120
+ col :sunday, required: true, &output_map( yes: ?1, no: ?0 )
121
+ col :start_date
122
+ col :end_date
123
+ end
124
+
125
+ file :calendar_dates do
126
+ col :service_id, required: true
127
+ col :date, required: true
128
+ col :exception_type, required: true,
129
+ &output_map( added: ?1, removed: ?2 )
130
+ end
131
+
132
+ file :fare_attributes do
133
+ col :fare_id, required: true, unique: true
134
+ col :price, required: true
135
+ col :currency_type, required: true
136
+ col :payment_method, required: true,
137
+ &output_map( on_board: 0, before: 1 )
138
+ col :transfers, required: true,
139
+ &output_map( :unlimited, none: 0, once: 1, twice: 2 )
140
+ col :transfer_duration
141
+ end
142
+
143
+ file :fare_rules do
144
+ col :fare_id, required: true
145
+ col :route_id
146
+ col :origin_id
147
+ col :destination_id
148
+ col :contains_id
149
+ end
150
+
151
+ file :shapes do
152
+ col :shape_id, required: true
153
+ col :shape_pt_lat, required: true
154
+ col :shape_pt_lon, required: true
155
+ col :shape_pt_sequence, required: true
156
+ col :shape_dist_traveled
157
+ end
158
+
159
+ file :frequencies do
160
+ col :trip_id, required: true
161
+ col :start_time, required: true
162
+ col :end_time, required: true
163
+ col :headway_secs, required: true
164
+ col :exact_times, &output_map( :inexact, exact: 1 )
165
+ end
166
+
167
+ file :transfers do
168
+ col :from_stop_id, required: true
169
+ col :to_stop_id, required: true
170
+ col :transfer_type, required: true,
171
+ &output_map( :recommended,
172
+ timed_transfer: 1,
173
+ minimum_time_required: 2,
174
+ impossible: 3 )
175
+ col :min_transfer_time
176
+ end
177
+
178
+ file :feed_info do
179
+ col :feed_publisher_name, required: true
180
+ col :feed_publisher_url, required: true
181
+ col :feed_lang, required: true
182
+ col :feed_start_date
183
+ col :feed_end_date
184
+ col :feed_version
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,55 @@
1
+ require_relative 'file_definition'
2
+
3
+ module GtfsReader
4
+ module Config
5
+ # Describes a GTFS feed and the {FileDefinition files} it is expected to
6
+ # provide.
7
+ class FeedDefinition
8
+ def initialize
9
+ @file_definition = {}
10
+ end
11
+
12
+ #@return [Array<FileDefinition>] All of the defined files.
13
+ def files
14
+ @file_definition.values
15
+ end
16
+
17
+ def required_files
18
+ files.select &:required?
19
+ end
20
+
21
+ def optional_files
22
+ files.reject &:required?
23
+ end
24
+
25
+ #@overload file(name, *args, &block)
26
+ # Defines a new file in the feed.
27
+ #
28
+ # @param name [String] the name of this file within the feed. This name
29
+ # should not include a file extension (like +.txt+)
30
+ # @param args [Array] the first argument is used as a +Hash+ of options
31
+ # to create the new file definition
32
+ # @param block [Proc] this block is +instance_eval+ed on the new
33
+ # {FileDefinition file}
34
+ # @return [FileDefinition] the newly created file
35
+ #
36
+ #@overload file(name)
37
+ # @param name [String] the name of the file to return
38
+ # @return [FileDefinition] the previously created file with the given name
39
+ #@see FileDefinition
40
+ def file(name, *args, &block)
41
+ return @file_definition[name] unless block_given?
42
+
43
+ definition_for!( name, args.first ).tap do |d|
44
+ d.instance_exec &block if block
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def definition_for!(name, opts)
51
+ @file_definition[name] ||= FileDefinition.new( name, opts )
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,94 @@
1
+ require_relative 'column'
2
+
3
+ module GtfsReader
4
+ module Config
5
+ # Describes a single file in a {FeedDefinition GTFS feed}.
6
+ class FileDefinition
7
+ attr_reader :name
8
+
9
+ #@param name [String] The name of the file within the feed.
10
+ #@option opts [Boolean] :required (false)
11
+ # If this file is required to be in the feed.
12
+ def initialize(name, opts={})
13
+ @name, @columns = name, {}
14
+ @opts = { required: false }.merge (opts || {})
15
+ end
16
+
17
+ #@return [Boolean] If this file is required to be in the feed.
18
+ def required?
19
+ @opts[:required]
20
+ end
21
+
22
+ #@return [String] The filename of this file within the GTFS feed.
23
+ def filename
24
+ "#{name}.txt"
25
+ end
26
+
27
+ #@return [Column] The column with the given name
28
+ def [](name)
29
+ @columns[name]
30
+ end
31
+
32
+ def columns
33
+ @columns.values
34
+ end
35
+
36
+ #@return [Array<Column>] The columns required to appear in this file.
37
+ def required_columns
38
+ columns.select &:required?
39
+ end
40
+
41
+ #@return [Array<Column>] The columns not required to appear in this file.
42
+ def optional_columns
43
+ columns.reject &:required?
44
+ end
45
+
46
+ #@return [Array<Column>] The columns which cannot have two rows with the
47
+ # same value.
48
+ def unique_columns
49
+ columns.select &:unique?
50
+ end
51
+
52
+ # Creates a column with the given name.
53
+ #
54
+ #@param name [String] The name of the column to define.
55
+ #@param args [Array] The first element of this args list is used as a
56
+ # +Hash+ of options to create the new column with.
57
+ #@param block [Proc] An optional block used to parse the values of this
58
+ # column on each row.
59
+ #@yieldparam input [String] The value of this column for a particular row.
60
+ #@yieldreturn Any kind of object.
61
+ #@return [Column] The newly created column.
62
+ def col(name, *args, &block)
63
+ if @columns.key? name
64
+ @columns[name].parser &block if block_given?
65
+ return @columns[name]
66
+ end
67
+
68
+ @columns[name] = Column.new name, args.first, &block
69
+ end
70
+
71
+ # Creates an input-output proc to convert column values from one form to
72
+ # another.
73
+ #
74
+ # Many parser procs simply map a set of known values to another set of
75
+ # known values. This helper creates such a proc from a given hash and
76
+ # optional default.
77
+ #
78
+ #@param default [] The value to return if there is no mapping for a given
79
+ # input.
80
+ #@param reverse_map [Hash] A map of returns values to their input values.
81
+ # This is in reverse because it looks better, like a list of labels:
82
+ # +{bus: 3, ferry: 4}+
83
+ def output_map(default=nil, reverse_map)
84
+ if reverse_map.values.uniq.length != reverse_map.values.length
85
+ raise FileDefinitionError, "Duplicate values given: #{reverse_map}"
86
+ end
87
+
88
+ map = default.nil? ? {} : Hash.new( default )
89
+ reverse_map.each { |k,v| map[v] = k }
90
+ map.method( :[] ).to_proc
91
+ end
92
+ end
93
+ end
94
+ end