gtfs_reader 1.0.0

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