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   53330 use Opendata::GTFS::Standard;
  4         505  
  4         286  
2 26     2   586252 use strict;
  26         73  
  26         124  
3 26     2   121 use warnings;
  26         609  
  4         21642  
4              
5             # PODNAME: Opendata::GTFS::Feed
6             # ABSTRACT: Parse General Transit Feeds (GTFS)
7              
8 4     2   3401 use Archive::Extract;
  4         276232  
  2         94  
9 2     2   780 use File::Temp;
  2         11327  
  2         196  
10 2     2   1038 use Text::CSV;
  2         23338  
  2         12  
11 2     2   1573 use Lingua::EN::Inflect;
  2         35430  
  2         223  
12              
13 2     2   1142 use File::BOM;
  2         38165  
  2         124  
14              
15 2     2   786 use Opendata::GTFS::Type::Agency;
  2         9  
  2         134  
16 2     2   1097 use Opendata::GTFS::Type::Calendar;
  2         6  
  2         88  
17 2     2   971 use Opendata::GTFS::Type::CalendarDate;
  2         6  
  2         93  
18 2     2   997 use Opendata::GTFS::Type::FareAttribute;
  2         7  
  2         141  
19 2     2   915 use Opendata::GTFS::Type::FareRule;
  2         5  
  2         86  
20 2     2   932 use Opendata::GTFS::Type::Frequency;
  2         5  
  2         86  
21 2     2   946 use Opendata::GTFS::Type::Route;
  2         6  
  2         140  
22 2     2   953 use Opendata::GTFS::Type::Shape;
  2         6  
  2         86  
23 2     2   966 use Opendata::GTFS::Type::Stop;
  2         6  
  2         94  
24 2     2   921 use Opendata::GTFS::Type::StopTime;
  2         6  
  2         88  
25 2     2   1041 use Opendata::GTFS::Type::Transfer;
  2         6  
  2         89  
26 2     2   1016 use Opendata::GTFS::Type::Trip;
  2         5  
  2         119  
27              
28 2     2   3260 class Opendata::GTFS::Feed using Moose {
  2     2   55  
  2     2   19  
  2         3  
  2         131  
  2         11  
  2         3  
  2         28  
  2         552  
  2         2  
  2         14  
  2         104  
  2         4  
  2         81  
  2         9  
  2         3  
  2         145  
  2         48  
  2         9  
  2         5  
  2         13  
  2         6184  
  2         3  
  2         13  
  2         10058  
  2         3  
  2         17  
  2         6282  
  2         4  
  2         27  
  2         156  
  2         2  
  2         16  
  2         392  
  2         2  
  2         15  
  2         1958  
  2         4  
  2         12  
  2         9157  
  2         7  
  2         80  
  2         9  
  2         1  
  2         75  
  2         8  
  2         3  
  2         97  
  2         8  
  2         2  
  2         284  
  2         17  
  2         7692  
  2         39  
  2         256  
29              
30 2         24 our $VERSION = '0.0103'; # VERSION
31              
32 2     2   21747 use Path::Tiny;
  2         4  
  2         129  
33 2     2   1161 use MooseX::AttributeDocumented;
  2         11555  
  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         10308 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         6738 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         6941 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   188273 fun type_to_singular($type) {
  2 50   94   4  
  2         360  
  2         107  
  47         206  
  47         18921  
  2         21  
76 2         8 my $name = $type->name;
77 2         8 $name =~ s{(?<=[a-z])([A-Z])}{_$1}g;
78 2         8 return lc $name;
79             }
80 2 50   2   2203 fun type_to_plural($type) {
  2 50   47   4  
  2         583  
  2         18  
  2         14  
  2         3  
  2         7  
81 2         9 my @names = split /_/ => type_to_singular($type);
82 0         0 $names[-1] = Lingua::EN::Inflect::PL($names[-1]);
83 2         15 return join '_' => @names;
84             }
85              
86 2         2 for (my $i = 0; $i < $#attributes; $i += 3) {
87 2         74 my $type = $attributes[$i];
88 94         213 my $attribute = type_to_plural($type);
89 94         204 my $singular = type_to_singular($type);
90              
91             has $attribute => (
92             is => 'ro',
93             isa => ArrayRef[ $type ],
94             traits => ['Array'],
95 94         346 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   4203 around BUILDARGS($orig: $self, @args) {
  2 50   2   18  
  2 50       1221  
  94 50       810  
  2         297  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
107 1         88 my %args = @args;
108 1 50       8 if(!exists $args{'directory'}) {
109 1         7 $args{'directory'} = File::Temp->newdir;
110             }
111 1         806362 $args{'directory'} = path($args{'directory'})->absolute;
112 1 50       392 if(exists $args{'file'}) {
    100          
113 1 0       5 if(path($args{'file'})->exists) {
114 1         9 $args{'file'} = path($args{'file'})->absolute;
115 1         21 my $x = Archive::Extract->new(archive => $args{'file'}->stringify);
116 1 0       12 $x->extract(to => $args{'directory'}->stringify) or die $x->error;
117             }
118             else {
119 1         118558 die sprintf 'Supplied filepath (%s) does not exist.', $args{'file'};
120             }
121             }
122             elsif(exists $args{'url'}) {
123 1     1   368 eval "use HTTP::Tiny";
124 2 50       115096 die "Passing 'url' to Opendata::GTFS::Feed->new requires HTTP::Tiny" if $@;
125              
126 2         8 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         50 $filename =~ s{/?\?.*}{};
132 24         32 $filename =~ s{.*/([^/]*)$}{$1};
133 24 50       51 $filename .= '.zip' if index ($filename, '.') == -1;
134              
135 24         731 $args{'directory'}->child($filename)->spew($response->{'content'});
136              
137 1         121 my $x = Archive::Extract->new(archive => $args{'directory'}->child($filename)->stringify);
138 23 50       1259 $x->extract(to => $args{'directory'}->stringify) or die $x->error;
139             }
140 23         52 $self->$orig(%args);
141             }
142              
143 2 50   2   2077 method BUILD {
  2     2   4  
  2         418  
  94         370  
  23         128  
  23         59  
144             FILE:
145 23         43 for (my $i = 0; $i < $#attributes; $i += 3) {
146 23         109 my $type = $attributes[$i];
147 23         42 my $is_required = $attributes[$i + 1];
148 23         24 my $filename = $attributes[$i + 2];
149              
150 23 100       44 if(!$self->directory->child($filename)->exists) {
151 23 50       53 next FILE if !$is_required;
152             }
153 23         212 $self->parse_file($type, $filename);
154              
155 23         2032 my $plural = type_to_plural($type);
156 23         675 my $method = "count_$plural";
157             }
158             }
159              
160 2 50   2   3353 method parse_file($type, $filename) {
  2 50   23   3  
  2 50       1179  
  47 50       136  
  23         3883  
  23         758  
  0         0  
  23         25  
  23         55  
161 23         76 my $method = sprintf 'add_%s', type_to_singular($type);
162 18         54 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   153 my $column_names = $csv->getline($fh);
  47         58  
  47         119  
  23         115  
169 153 50       3515 if(!defined $column_names) {
170 153         4408 die sprintf "Can't read the first line of the file. Check %s for errors.", $self->directory->child($filename);
171             }
172 152         757 my @column_names = @{ $column_names };
  152         145  
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   265 if($type->name eq 'StopTime' && any { $_ eq 'drop_off_time' } @column_names) {
  152         182  
177 152     0   861 my $index = first_index { $_ eq 'drop_off_time'} @column_names;
  152         4977  
178              
179 152 0       1977 $column_names[ $index ] = 'drop_off_type' if $index >= 0;
180             }
181              
182             LINE:
183 23         536 while(1) {
184 1         8 my $line = $csv->getline($fh);
185 1 100 100     2 last LINE if $csv->eof && !defined $line;
186 1 50       15 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.0103, released 2015-02-21.
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