File Coverage

blib/lib/Opendata/GTFS/Feed.pm
Criterion Covered Total %
statement 220 230 95.6
branch 32 64 50.0
condition 6 12 50.0
subroutine 38 39 97.4
pod n/a
total 296 345 85.8


line stmt bran cond sub pod time code
1 2     2   142078 use Opendata::GTFS::Standard;
  4         519  
  4         285  
2 26     2   579893 use strict;
  26         74  
  26         126  
3 26     2   124 use warnings;
  26         595  
  4         21301  
4              
5             # PODNAME: Opendata::GTFS::Feed
6             # ABSTRACT: Parse General Transit Feeds (GTFS)
7              
8 4     2   3896 use Archive::Extract;
  4         281439  
  2         85  
9 2     2   701 use File::Temp;
  2         11058  
  2         172  
10 2     2   1018 use Text::CSV;
  2         23063  
  2         12  
11 2     2   1643 use Lingua::EN::Inflect;
  2         35733  
  2         199  
12              
13 2     2   1153 use File::BOM;
  2         39817  
  2         117  
14              
15 2     2   833 use Opendata::GTFS::Type::Agency;
  2         6  
  2         107  
16 2     2   1046 use Opendata::GTFS::Type::Calendar;
  2         5  
  2         92  
17 2     2   941 use Opendata::GTFS::Type::CalendarDate;
  2         5  
  2         104  
18 2     2   943 use Opendata::GTFS::Type::FareAttribute;
  2         4  
  2         106  
19 2     2   984 use Opendata::GTFS::Type::FareRule;
  2         4  
  2         101  
20 2     2   937 use Opendata::GTFS::Type::Frequency;
  2         7  
  2         95  
21 2     2   982 use Opendata::GTFS::Type::Route;
  2         7  
  2         94  
22 2     2   927 use Opendata::GTFS::Type::Shape;
  2         6  
  2         118  
23 2     2   1024 use Opendata::GTFS::Type::Stop;
  2         6  
  2         93  
24 2     2   1119 use Opendata::GTFS::Type::StopTime;
  2         9  
  2         99  
25 2     2   1047 use Opendata::GTFS::Type::Transfer;
  2         5  
  2         83  
26 2     2   940 use Opendata::GTFS::Type::Trip;
  2         7  
  2         144  
27              
28 2     2   3158 class Opendata::GTFS::Feed using Moose {
  2     2   56  
  2     2   15  
  2         3  
  2         129  
  2         9  
  2         3  
  2         19  
  2         587  
  2         3  
  2         13  
  2         107  
  2         4  
  2         155  
  2         9  
  2         3  
  2         155  
  2         52  
  2         9  
  2         3  
  2         12  
  2         5860  
  2         3  
  2         15  
  2         10215  
  2         4  
  2         15  
  2         6242  
  2         5  
  2         18  
  2         138  
  2         3  
  2         18  
  2         450  
  2         3  
  2         16  
  2         1919  
  2         3  
  2         14  
  2         8546  
  2         6  
  2         74  
  2         8  
  2         3  
  2         65  
  2         9  
  2         6  
  2         105  
  2         8  
  2         3  
  2         288  
  2         17  
  2         7380  
  2         34  
  2         286  
29              
30 2         23 our $VERSION = '0.0104'; # VERSION
31              
32 2     2   20419 use Path::Tiny;
  2         4  
  2         99  
33 2     2   1175 use MooseX::AttributeDocumented;
  2         10886  
  2         9  
34              
35 2         9 has file => (
36             is => 'ro',
37             isa => AbsPath,
38             required => 0,
39             coerce => 1,
40             documentation_order => 1,
41             documentation => q{If file is given, the feed in the file will be parsed.},
42             );
43 2         10104 has directory => (
44             is => 'ro',
45             isa => AbsPath,
46             required => 0,
47             coerce => 1,
48             documentation_order => 3,
49             documentation => q{If only directory is given, it is expected to find a fully extracted feed in that directory. If C<directory> is given together with either file or url, the feed will be extracted into that directory (and remain there).},
50             );
51 2         6270 has url => (
52             is => 'ro',
53             isa => Uri,
54             required => 0,
55             coerce => 1,
56             documentation_order => 1,
57             documentation => q{If url is given, the feed at the url will be fetched and parsed.},
58             );
59              
60 2         6360 my @attributes = (
61             Agency, 1 => 'agency.txt',
62             Stop, 1 => 'stops.txt',
63             Route, 1 => 'routes.txt',
64             Trip, 1 => 'trips.txt',
65             StopTime, 1 => 'stop_times.txt',
66             Calendar, 1 => 'calendar.txt',
67             CalendarDate, 0 => 'calendar_dates.txt',
68             FareAttribute, 0 => 'fare_attributes.txt',
69             FareRule, 0 => 'fare_rules.txt',
70             Shape, 0 => 'shapes.txt',
71             Frequency, 0 => 'frequencies.txt',
72             Transfer, 0 => 'transfers.txt',
73             );
74              
75 2 50   2   178456 fun type_to_singular($type) {
  2 50   94   5  
  2         335  
  2         105  
  47         184  
  47         19071  
  2         19  
76 2         9 my $name = $type->name;
77 2         8 $name =~ s{(?<=[a-z])([A-Z])}{_$1}g;
78 2         7 return lc $name;
79             }
80 2 50   2   2100 fun type_to_plural($type) {
  2 50   47   4  
  2         601  
  2         17  
  2         14  
  2         3  
  2         8  
81 2         14 my @names = split /_/ => type_to_singular($type);
82 0         0 $names[-1] = Lingua::EN::Inflect::PL($names[-1]);
83 2         12 return join '_' => @names;
84             }
85              
86 2         3 for (my $i = 0; $i < $#attributes; $i += 3) {
87 2         71 my $type = $attributes[$i];
88 94         206 my $attribute = type_to_plural($type);
89 94         214 my $singular = type_to_singular($type);
90              
91             has $attribute => (
92             is => 'ro',
93             isa => ArrayRef[ $type ],
94             traits => ['Array'],
95 94         332 default => sub { [] },
96 94         95 init_arg => undef,
97             handles => {
98             "add_$singular" => 'push',
99             "all_$attribute" => 'elements',
100             "count_$attribute" => 'count',
101             },
102             documentation_default => '[]',
103             );
104             }
105              
106 2 50   2   4076 around BUILDARGS($orig: $self, @args) {
  2 50   2   4  
  2 50       1326  
  94 50       891  
  2         299  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
107 1         158 my %args = @args;
108 1 50       8 if(!exists $args{'directory'}) {
109 1         6 $args{'directory'} = File::Temp->newdir;
110             }
111 1         440953 $args{'directory'} = path($args{'directory'})->absolute;
112 1 50       331 if(exists $args{'file'}) {
    100          
113 1 0       3 if(path($args{'file'})->exists) {
114 1         9 $args{'file'} = path($args{'file'})->absolute;
115 1         6 my $x = Archive::Extract->new(archive => $args{'file'}->stringify);
116 1 0       10 $x->extract(to => $args{'directory'}->stringify) or die $x->error;
117             }
118             else {
119 1         501 die sprintf 'Supplied filepath (%s) does not exist.', $args{'file'};
120             }
121             }
122             elsif(exists $args{'url'}) {
123 1     1   261 eval "use HTTP::Tiny";
124 2 50       86424 die "Passing 'url' to Opendata::GTFS::Feed->new requires HTTP::Tiny" if $@;
125              
126 2         10 my $response = HTTP::Tiny->new->get($args{'url'});
127              
128 2 50       4 die sprintf "Can't download %s: %s", $args{'url'}, join (' - ' => $response->{'status'}, $response->{'reason'}) if !$response->{'success'};
129              
130 2         10 my $filename = $args{'url'};
131 24         55 $filename =~ s{/?\?.*}{};
132 24         31 $filename =~ s{.*/([^/]*)$}{$1};
133 24 50       52 $filename .= '.zip' if index ($filename, '.') == -1;
134              
135 24         851 $args{'directory'}->child($filename)->spew($response->{'content'});
136              
137 1         139 my $x = Archive::Extract->new(archive => $args{'directory'}->child($filename)->stringify);
138 23 50       1283 $x->extract(to => $args{'directory'}->stringify) or die $x->error;
139             }
140 23         52 $self->$orig(%args);
141             }
142              
143 2 50   2   1977 method BUILD {
  2     2   3  
  2         419  
  94         374  
  23         140  
  23         64  
144             FILE:
145 23         53 for (my $i = 0; $i < $#attributes; $i += 3) {
146 23         43 my $type = $attributes[$i];
147 23         44 my $is_required = $attributes[$i + 1];
148 23         21 my $filename = $attributes[$i + 2];
149              
150 23 100       41 if(!$self->directory->child($filename)->exists) {
151 23 50       55 next FILE if !$is_required;
152             }
153 23         197 $self->parse_file($type, $filename);
154              
155 23         2116 my $plural = type_to_plural($type);
156 23         756 my $method = "count_$plural";
157             }
158             }
159              
160 2 50   2   3259 method parse_file($type, $filename) {
  2 50   23   3  
  2 50       1207  
  47 50       126  
  23         4112  
  23         817  
  0         0  
  23         31  
  23         59  
161 23         72 my $method = sprintf 'add_%s', type_to_singular($type);
162 18         47 my $class = sprintf 'Opendata::GTFS::Type::%s', $type->name;
163              
164 0         0 my $csv = Text::CSV->new( { binary => 1 } );
165 0         0 my $fh;
166 0         0 File::BOM::open_bom($fh, $self->directory->child($filename), ':utf8');
167              
168 47     2   132 my $column_names = $csv->getline($fh);
  47         49  
  47         111  
  23         129  
169 153 50       3743 if(!defined $column_names) {
170 153         4442 die sprintf "Can't read the first line of the file. Check %s for errors.", $self->directory->child($filename);
171             }
172 152         802 my @column_names = @{ $column_names };
  152         127  
173              
174             # Google's example feed (https://developers.google.com/transit/gtfs/examples/gtfs-feed / https://developers.google.com/transit/gtfs/examples/sample-feed.zip)
175             # has a (reported) bug. This fixes that.
176 152 50 66 18   257 if($type->name eq 'StopTime' && any { $_ eq 'drop_off_time' } @column_names) {
  152         188  
177 152     0   892 my $index = first_index { $_ eq 'drop_off_time'} @column_names;
  152         5045  
178              
179 152 0       2109 $column_names[ $index ] = 'drop_off_type' if $index >= 0;
180             }
181              
182             LINE:
183 23         547 while(1) {
184 1         9 my $line = $csv->getline($fh);
185 1 100 100     1 last LINE if $csv->eof && !defined $line;
186 1 50       19 next LINE if !defined $line;
187   0 0       next LINE if scalar @{ $line } == 1 && (!defined $line->[0] || $line->[0] eq ''); # skip empty lines
      33        
188              
189             my @args = zip @column_names, @{ $line };
190             $self->$method($class->new(@args));
191              
192   100         last LINE if $csv->eof;
193             }
194              
195   50         close $fh or die sprintf "Can't close %s", $self->directory->child($filename);
196             }
197             }
198              
199             1;
200              
201             __END__
202              
203             =pod
204              
205             =encoding utf-8
206              
207             =head1 NAME
208              
209             Opendata::GTFS::Feed - Parse General Transit Feeds (GTFS)
210              
211             =head1 VERSION
212              
213             Version 0.0104, released 2015-02-22.
214              
215              
216              
217             =head1 SYNOPSIS
218              
219             use Opendata::GTFS::Feed;
220             my $feed = Opendata::GTFS::Feed->parse(file => 'a-gtfs-feed.zip', directory => 'feed');
221              
222             =head1 DESCRIPTION
223              
224             Opendata::GTFS::Feed is an easy way to parse L<GTFS|https://developers.google.com/transit/gtfs/> feeds.
225              
226             =head1 ATTRIBUTES
227              
228             All list attributes has the L<Array|Moose::Meta::Attribute::Native::Trait::Array> trait. Currently the following public methods are created for those attributes:
229              
230             =over 4
231              
232             =item *
233              
234             C<elements> -E<gt> C<all_$attribute>, where C<$attribute> is the attribute name.
235              
236             =item *
237              
238             C<count> -E<gt> C<count_$attribute>
239              
240             =back
241              
242              
243             =head2 file
244              
245             =begin HTML
246              
247             <table cellpadding="0" cellspacing="0">
248             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Path::Tiny#AbsPath">AbsPath</a>
249              
250             </td>
251             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">optional</td>
252             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
253             </table>
254              
255             <p>If file is given, the feed in the file will be parsed.</p>
256              
257             =end HTML
258              
259             =head2 url
260              
261             =begin HTML
262              
263             <table cellpadding="0" cellspacing="0">
264             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::URI#Uri">Uri</a>
265              
266             </td>
267             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">optional</td>
268             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
269             </table>
270              
271             <p>If url is given, the feed at the url will be fetched and parsed.</p>
272              
273             =end HTML
274              
275             =head2 directory
276              
277             =begin HTML
278              
279             <table cellpadding="0" cellspacing="0">
280             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Path::Tiny#AbsPath">AbsPath</a>
281              
282             </td>
283             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">optional</td>
284             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
285             </table>
286              
287             <p>If only directory is given, it is expected to find a fully extracted feed in that directory. If C<directory> is given together with either file or url, the feed will be extracted into that directory (and remain there).</p>
288              
289             =end HTML
290              
291             =head2 agencies
292              
293             =begin HTML
294              
295             <table cellpadding="0" cellspacing="0">
296             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Agency">Agency</a> ]</td>
297             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
298             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
299             </table>
300              
301             <p></p>
302              
303             =end HTML
304              
305             =head2 calendar_dates
306              
307             =begin HTML
308              
309             <table cellpadding="0" cellspacing="0">
310             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#CalendarDate">CalendarDate</a> ]</td>
311             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
312             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
313             </table>
314              
315             <p></p>
316              
317             =end HTML
318              
319             =head2 calendars
320              
321             =begin HTML
322              
323             <table cellpadding="0" cellspacing="0">
324             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Calendar">Calendar</a> ]</td>
325             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
326             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
327             </table>
328              
329             <p></p>
330              
331             =end HTML
332              
333             =head2 fare_attributes
334              
335             =begin HTML
336              
337             <table cellpadding="0" cellspacing="0">
338             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#FareAttribute">FareAttribute</a> ]</td>
339             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
340             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
341             </table>
342              
343             <p></p>
344              
345             =end HTML
346              
347             =head2 fare_rules
348              
349             =begin HTML
350              
351             <table cellpadding="0" cellspacing="0">
352             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#FareRule">FareRule</a> ]</td>
353             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
354             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
355             </table>
356              
357             <p></p>
358              
359             =end HTML
360              
361             =head2 frequencies
362              
363             =begin HTML
364              
365             <table cellpadding="0" cellspacing="0">
366             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Frequency">Frequency</a> ]</td>
367             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
368             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
369             </table>
370              
371             <p></p>
372              
373             =end HTML
374              
375             =head2 routes
376              
377             =begin HTML
378              
379             <table cellpadding="0" cellspacing="0">
380             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Route">Route</a> ]</td>
381             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
382             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
383             </table>
384              
385             <p></p>
386              
387             =end HTML
388              
389             =head2 shapes
390              
391             =begin HTML
392              
393             <table cellpadding="0" cellspacing="0">
394             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Shape">Shape</a> ]</td>
395             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
396             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
397             </table>
398              
399             <p></p>
400              
401             =end HTML
402              
403             =head2 stop_times
404              
405             =begin HTML
406              
407             <table cellpadding="0" cellspacing="0">
408             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#StopTime">StopTime</a> ]</td>
409             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
410             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
411             </table>
412              
413             <p></p>
414              
415             =end HTML
416              
417             =head2 stops
418              
419             =begin HTML
420              
421             <table cellpadding="0" cellspacing="0">
422             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Stop">Stop</a> ]</td>
423             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
424             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
425             </table>
426              
427             <p></p>
428              
429             =end HTML
430              
431             =head2 transfers
432              
433             =begin HTML
434              
435             <table cellpadding="0" cellspacing="0">
436             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Transfer">Transfer</a> ]</td>
437             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
438             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
439             </table>
440              
441             <p></p>
442              
443             =end HTML
444              
445             =head2 trips
446              
447             =begin HTML
448              
449             <table cellpadding="0" cellspacing="0">
450             <tr><td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;"><a href="https://metacpan.org/pod/Types::Standard#ArrayRef">ArrayRef</a> [ <a href="https://metacpan.org/pod/Types::Standard#Trip">Trip</a> ]</td>
451             <td style="padding-right: 6px; padding-left: 6px; border-right: 1px solid #b8b8b8; white-space: nowrap;">not in constructor</td>
452             <td style="padding-left: 6px; padding-right: 6px; white-space: nowrap;">read-only</td></tr>
453             </table>
454              
455             <p></p>
456              
457             =end HTML
458              
459             =head1 SOURCE
460              
461             L<https://github.com/Csson/p5-Opendata-GTFS-Feed>
462              
463             =head1 HOMEPAGE
464              
465             L<https://metacpan.org/release/Opendata-GTFS-Feed>
466              
467             =head1 AUTHOR
468              
469             Erik Carlsson <info@code301.com>
470              
471             =head1 COPYRIGHT AND LICENSE
472              
473             This software is copyright (c) 2015 by Erik Carlsson <info@code301.com>.
474              
475             This is free software; you can redistribute it and/or modify it under
476             the same terms as the Perl 5 programming language system itself.
477              
478             =cut