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