gtfs_reader 1.0.0
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 +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
|