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.
- checksums.yaml +7 -0
- data/LICENSE +284 -0
- data/README.md +96 -0
- data/Rakefile +53 -0
- data/lib/gtfs_reader.rb +7 -0
- data/lib/gtfs_reader/bulk_feed_handler.rb +94 -0
- data/lib/gtfs_reader/config/column.rb +60 -0
- data/lib/gtfs_reader/config/defaults/gtfs_feed_definition.rb +190 -0
- data/lib/gtfs_reader/config/feed_definition.rb +55 -0
- data/lib/gtfs_reader/config/file_definition.rb +94 -0
- data/lib/gtfs_reader/config/source.rb +68 -0
- data/lib/gtfs_reader/config/sources.rb +25 -0
- data/lib/gtfs_reader/configuration.rb +27 -0
- data/lib/gtfs_reader/core.rb +78 -0
- data/lib/gtfs_reader/exceptions.rb +29 -0
- data/lib/gtfs_reader/feed_handler.rb +33 -0
- data/lib/gtfs_reader/file_reader.rb +120 -0
- data/lib/gtfs_reader/file_row.rb +66 -0
- data/lib/gtfs_reader/log.rb +70 -0
- data/lib/gtfs_reader/source_updater.rb +194 -0
- data/lib/gtfs_reader/version.rb +77 -0
- metadata +193 -0
@@ -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
|