gtfs-reader 0.2.2
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 +57 -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 +72 -0
- data/lib/gtfs_reader/config/defaults/gtfs_feed_definition.rb +208 -0
- data/lib/gtfs_reader/config/feed_definition.rb +55 -0
- data/lib/gtfs_reader/config/file_definition.rb +111 -0
- data/lib/gtfs_reader/config/prefixed_column_setter.rb +26 -0
- data/lib/gtfs_reader/config/source.rb +61 -0
- data/lib/gtfs_reader/config/sources.rb +25 -0
- data/lib/gtfs_reader/configuration.rb +27 -0
- data/lib/gtfs_reader/core.rb +76 -0
- data/lib/gtfs_reader/exceptions.rb +29 -0
- data/lib/gtfs_reader/feed_handler.rb +30 -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 +66 -0
- data/lib/gtfs_reader/source_updater.rb +130 -0
- data/lib/gtfs_reader/version.rb +71 -0
- metadata +195 -0
@@ -0,0 +1,72 @@
|
|
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 [String] :alias an alternative name for this column. Many
|
14
|
+
# column names are needlessly prefixed with their filename:
|
15
|
+
# +Stop.stop_name+ could be aliased to +Stop.name+ for example.
|
16
|
+
#@option opts [Boolean] :unique (false) if values in this column need to be
|
17
|
+
# unique among all rows in the file.
|
18
|
+
def initialize(name, opts={}, &parser)
|
19
|
+
@name = name
|
20
|
+
@parser = block_given? ? parser : IDENTITY_PARSER
|
21
|
+
|
22
|
+
@opts = {
|
23
|
+
required: false,
|
24
|
+
unique: false,
|
25
|
+
alias: nil
|
26
|
+
}.merge (opts || {})
|
27
|
+
end
|
28
|
+
|
29
|
+
def parser(&block)
|
30
|
+
@parser = block if block_given?
|
31
|
+
@parser
|
32
|
+
end
|
33
|
+
|
34
|
+
#@return [Boolean] if this column is required to appear in the file
|
35
|
+
def required?
|
36
|
+
@opts[:required]
|
37
|
+
end
|
38
|
+
|
39
|
+
#@return [Boolean] if values in this column need to be unique among all rows
|
40
|
+
# in the file.
|
41
|
+
def unique?
|
42
|
+
@opts[:unique]
|
43
|
+
end
|
44
|
+
|
45
|
+
#@return [String,nil] this column's name's alias, if there is one
|
46
|
+
def alias
|
47
|
+
@opts[:alias]
|
48
|
+
end
|
49
|
+
|
50
|
+
#@return [Boolean]
|
51
|
+
def parser?
|
52
|
+
parser != IDENTITY_PARSER
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
opts = @opts.collect do |key,value|
|
57
|
+
case value
|
58
|
+
when true then key
|
59
|
+
when false,nil then nil
|
60
|
+
else "#{key}=#{value}"
|
61
|
+
end
|
62
|
+
end.reject &:nil?
|
63
|
+
|
64
|
+
opts << 'has_parser' if parser?
|
65
|
+
|
66
|
+
str = name.to_s
|
67
|
+
str << ": #{opts.join ', '}" unless opts.empty?
|
68
|
+
"[#{str}]"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require_relative '../feed_definition'
|
2
|
+
|
3
|
+
module GtfsReader
|
4
|
+
module Config
|
5
|
+
module Defaults
|
6
|
+
FEED_DEFINITION = FeedDefinition.new.tap do |feed|
|
7
|
+
feed.instance_exec do
|
8
|
+
file :agency, required: true do
|
9
|
+
prefix :agency do
|
10
|
+
col :name, required: true
|
11
|
+
col :url, required: true
|
12
|
+
col :timezone, required: true
|
13
|
+
col :id, unique: true
|
14
|
+
col :lang
|
15
|
+
col :phone
|
16
|
+
col :fare_url
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
file :stops, required: true do
|
21
|
+
prefix :stop do
|
22
|
+
col :id, required: true, unique: true
|
23
|
+
col :code
|
24
|
+
col :name, required: true
|
25
|
+
col :desc
|
26
|
+
col :lat, required: true
|
27
|
+
col :lon, required: true
|
28
|
+
col :url
|
29
|
+
col :timezone
|
30
|
+
end
|
31
|
+
|
32
|
+
col :zone_id
|
33
|
+
col :location_type, &output_map( :stop, station: ?1 )
|
34
|
+
col :parent_station
|
35
|
+
|
36
|
+
col :wheelchair_boarding do |val|
|
37
|
+
if parent_station
|
38
|
+
case val
|
39
|
+
when ?2 then :no
|
40
|
+
when ?1 then :yes
|
41
|
+
else :inherit
|
42
|
+
end
|
43
|
+
else
|
44
|
+
case val
|
45
|
+
when ?2 then :no_vehicles
|
46
|
+
when ?1 then :some_vehicles
|
47
|
+
else :unknown
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
file :routes, required: true do
|
54
|
+
prefix :route do
|
55
|
+
col :id, required: true, unique: true
|
56
|
+
col :short_name, required: true
|
57
|
+
col :long_name, required: true
|
58
|
+
col :desc
|
59
|
+
col :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 :url
|
70
|
+
col :color
|
71
|
+
col :text_color
|
72
|
+
end
|
73
|
+
|
74
|
+
col :agency_id
|
75
|
+
end
|
76
|
+
|
77
|
+
file :trips, required: true do
|
78
|
+
prefix :trip do
|
79
|
+
col :id, required: true, unique: true
|
80
|
+
col :headsign
|
81
|
+
col :short_name
|
82
|
+
col :long_name
|
83
|
+
end
|
84
|
+
|
85
|
+
col :route_id, required: true
|
86
|
+
col :service_id, required: true
|
87
|
+
col :direction_id,
|
88
|
+
&output_map( primary: ?0, opposite: ?1 )
|
89
|
+
col :block_id
|
90
|
+
col :shape_id
|
91
|
+
col :wheelchair_accessible,
|
92
|
+
&output_map( :unknown, yes: ?1, no: ?2 )
|
93
|
+
col :bikes_allowed,
|
94
|
+
&output_map( :unknown, yes: ?1, no: ?2 )
|
95
|
+
end
|
96
|
+
|
97
|
+
file :stop_times, required: true do
|
98
|
+
col :trip_id, required: true
|
99
|
+
col :arrival_time, required: true
|
100
|
+
col :departure_time, required: true
|
101
|
+
|
102
|
+
prefix :stop do
|
103
|
+
col :id, required: true
|
104
|
+
col :sequence, required: true
|
105
|
+
col :headsign
|
106
|
+
end
|
107
|
+
|
108
|
+
col :pickup_type,
|
109
|
+
&output_map( :regular,
|
110
|
+
none: ?1,
|
111
|
+
phone_agency: ?2,
|
112
|
+
coordinate_with_driver: ?3 )
|
113
|
+
|
114
|
+
col :drop_off_type,
|
115
|
+
&output_map( :regular,
|
116
|
+
none: ?1,
|
117
|
+
phone_agency: ?2,
|
118
|
+
coordinate_with_driver: ?3 )
|
119
|
+
|
120
|
+
col :shape_dist_traveled
|
121
|
+
end
|
122
|
+
|
123
|
+
file :calendar, required: true do
|
124
|
+
col :service_id, required: true, unique: true
|
125
|
+
|
126
|
+
col :monday, required: true, &output_map( yes: ?1, no: ?0 )
|
127
|
+
col :tuesday, required: true, &output_map( yes: ?1, no: ?0 )
|
128
|
+
col :wednesday, required: true, &output_map( yes: ?1, no: ?0 )
|
129
|
+
col :thursday, required: true, &output_map( yes: ?1, no: ?0 )
|
130
|
+
col :friday, required: true, &output_map( yes: ?1, no: ?0 )
|
131
|
+
col :saturday, required: true, &output_map( yes: ?1, no: ?0 )
|
132
|
+
col :sunday, required: true, &output_map( yes: ?1, no: ?0 )
|
133
|
+
|
134
|
+
col :start_date
|
135
|
+
col :end_date
|
136
|
+
end
|
137
|
+
|
138
|
+
file :calendar_dates do
|
139
|
+
col :service_id, required: true
|
140
|
+
col :date, required: true
|
141
|
+
col :exception_type, required: true,
|
142
|
+
&output_map( added: ?1, removed: ?2 )
|
143
|
+
end
|
144
|
+
|
145
|
+
file :fare_attributes do
|
146
|
+
col :fare_id, required: true, unique: true
|
147
|
+
col :price, required: true
|
148
|
+
col :currency_type, required: true
|
149
|
+
col :payment_method, required: true,
|
150
|
+
&output_map( on_board: 0, before: 1 )
|
151
|
+
col :transfers, required: true,
|
152
|
+
&output_map( :unlimited, none: 0, once: 1, twice: 2 )
|
153
|
+
col :transfer_duration
|
154
|
+
end
|
155
|
+
|
156
|
+
file :fare_rules do
|
157
|
+
col :fare_id, required: true
|
158
|
+
col :route_id
|
159
|
+
col :origin_id
|
160
|
+
col :destination_id
|
161
|
+
col :contains_id
|
162
|
+
end
|
163
|
+
|
164
|
+
file :shapes do
|
165
|
+
prefix :shape do
|
166
|
+
col :id, required: true
|
167
|
+
col :pt_lat, required: true
|
168
|
+
col :pt_lon, required: true
|
169
|
+
col :pt_sequence, required: true
|
170
|
+
col :dist_traveled
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
file :frequencies do
|
175
|
+
col :trip_id, required: true
|
176
|
+
col :start_time, required: true
|
177
|
+
col :end_time, required: true
|
178
|
+
col :headway_secs, required: true
|
179
|
+
col :exact_times,
|
180
|
+
&output_map( :inexact, exact: 1 )
|
181
|
+
end
|
182
|
+
|
183
|
+
file :transfers do
|
184
|
+
col :from_stop_id, required: true
|
185
|
+
col :to_stop_id, required: true
|
186
|
+
col :transfer_type, required: true,
|
187
|
+
&output_map( :recommended,
|
188
|
+
timed_transfer: 1,
|
189
|
+
minimum_time_required: 2,
|
190
|
+
impossible: 3 )
|
191
|
+
col :min_transfer_time
|
192
|
+
end
|
193
|
+
|
194
|
+
file :feed_info do
|
195
|
+
prefix :scope do
|
196
|
+
col :publisher_name, required: true
|
197
|
+
col :publisher_url, required: true
|
198
|
+
col :lang, required: true
|
199
|
+
col :start_date
|
200
|
+
col :end_date
|
201
|
+
col :version
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
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,111 @@
|
|
1
|
+
require_relative 'column'
|
2
|
+
require_relative 'prefixed_column_setter'
|
3
|
+
|
4
|
+
module GtfsReader
|
5
|
+
module Config
|
6
|
+
# Describes a single file in a {FeedDefinition GTFS feed}.
|
7
|
+
class FileDefinition
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
#@param name [String] The name of the file within the feed.
|
11
|
+
#@option opts [Boolean] :required (false)
|
12
|
+
# If this file is required to be in the feed.
|
13
|
+
def initialize(name, opts={})
|
14
|
+
@name, @columns = name, {}
|
15
|
+
@opts = { required: false }.merge (opts || {})
|
16
|
+
@aliases = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
#@return [Boolean] If this file is required to be in the feed.
|
20
|
+
def required?
|
21
|
+
@opts[:required]
|
22
|
+
end
|
23
|
+
|
24
|
+
#@return [String] The filename of this file within the GTFS feed.
|
25
|
+
def filename
|
26
|
+
"#{name}.txt"
|
27
|
+
end
|
28
|
+
|
29
|
+
#@return [Column] The column with the given name
|
30
|
+
def [](name)
|
31
|
+
@columns[name]
|
32
|
+
end
|
33
|
+
|
34
|
+
def columns
|
35
|
+
@columns.values
|
36
|
+
end
|
37
|
+
|
38
|
+
#@return [Array<Column>] The columns required to appear in this file.
|
39
|
+
def required_columns
|
40
|
+
columns.select &:required?
|
41
|
+
end
|
42
|
+
|
43
|
+
#@return [Array<Column>] The columns not required to appear in this file.
|
44
|
+
def optional_columns
|
45
|
+
columns.reject &:required?
|
46
|
+
end
|
47
|
+
|
48
|
+
#@return [Array<Column>] The columns which cannot have two rows with the
|
49
|
+
# same value.
|
50
|
+
def unique_columns
|
51
|
+
columns.select &:unique?
|
52
|
+
end
|
53
|
+
|
54
|
+
# Creates a column with the given name.
|
55
|
+
#
|
56
|
+
#@param name [String] The name of the column to define.
|
57
|
+
#@param args [Array] The first element of this args list is used as a
|
58
|
+
# +Hash+ of options to create the new column with.
|
59
|
+
#@param block [Proc] An optional block used to parse the values of this
|
60
|
+
# column on each row.
|
61
|
+
#@yieldparam input [String] The value of this column for a particular row.
|
62
|
+
#@yieldreturn Any kind of object.
|
63
|
+
#@return [Column] The newly created column.
|
64
|
+
def col(name, *args, &block)
|
65
|
+
name = @aliases[name] if @aliases.key? name
|
66
|
+
|
67
|
+
if @columns.key? name
|
68
|
+
@columns[name].parser &block if block_given?
|
69
|
+
return @columns[name]
|
70
|
+
end
|
71
|
+
|
72
|
+
(@columns[name] = Column.new name, args.first, &block).tap do |col|
|
73
|
+
@aliases[col.alias] = name if col.alias
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Starts a new block within which any defined columns will have the given
|
78
|
+
# +sym+ prefixed to its name (joined with an underscore). Also, the
|
79
|
+
# defined name given within the block will be aliased to the column.
|
80
|
+
#@param sym the prefix to prefixed to each column within the block
|
81
|
+
#
|
82
|
+
#@example Create a column +route_name+ with the alias +name+
|
83
|
+
# prefix( :route ) { name }
|
84
|
+
def prefix(sym, &blk)
|
85
|
+
PrefixedColumnSetter.new(self, sym.to_s).instance_exec &blk
|
86
|
+
end
|
87
|
+
|
88
|
+
# Creates an input-output proc to convert column values from one form to
|
89
|
+
# another.
|
90
|
+
#
|
91
|
+
# Many parser procs simply map a set of known values to another set of
|
92
|
+
# known values. This helper creates such a proc from a given hash and
|
93
|
+
# optional default.
|
94
|
+
#
|
95
|
+
#@param default [] The value to return if there is no mapping for a given
|
96
|
+
# input.
|
97
|
+
#@param reverse_map [Hash] A map of returns values to their input values.
|
98
|
+
# This is in reverse because it looks better, like a list of labels:
|
99
|
+
# +{bus: 3, ferry: 4}+
|
100
|
+
def output_map(default=nil, reverse_map)
|
101
|
+
if reverse_map.values.uniq.length != reverse_map.values.length
|
102
|
+
raise FileDefinitionError, "Duplicate values given: #{reverse_map}"
|
103
|
+
end
|
104
|
+
|
105
|
+
map = default.nil? ? {} : Hash.new( default )
|
106
|
+
reverse_map.each { |k,v| map[v] = k }
|
107
|
+
map.method( :[] ).to_proc
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|