File Coverage

blib/lib/Opendata/GTFS/Feed.pm
Criterion Covered Total %
statement 223 230 96.9
branch 34 64 53.1
condition 7 12 58.3
subroutine 39 39 100.0
pod n/a
total 303 345 87.8


line stmt bran cond sub pod time code
1 2     2   52030 use Opendata::GTFS::Standard;
  4         479  
  4         245  
2 26     2   588904 use strict;
  26         87  
  26         114  
3 26     2   139 use warnings;
  26         830  
  4         22991  
4              
5             # PODNAME: Opendata::GTFS::Feed
6             # ABSTRACT: Parse General Transit Feeds (GTFS)
7              
8 4     2   3621 use Archive::Extract;
  4         284085  
  2         86  
9 2     2   685 use File::Temp;
  2         10435  
  2         187  
10 2     2   1028 use Text::CSV;
  2         23150  
  2         11  
11 2     2   1668 use Lingua::EN::Inflect;
  2         35626  
  2         187  
12              
13 2     2   1200 use File::BOM;
  2         38092  
  2         114  
14              
15 2     2   811 use Opendata::GTFS::Type::Agency;
  2         7  
  2         100  
16 2     2   1141 use Opendata::GTFS::Type::Calendar;
  2         4  
  2         90  
17 2     2   991 use Opendata::GTFS::Type::CalendarDate;
  2         6  
  2         91  
18 2     2   935 use Opendata::GTFS::Type::FareAttribute;
  2         11  
  2         87  
19 2     2   920 use Opendata::GTFS::Type::FareRule;
  2         9  
  2         101  
20 2     2   928 use Opendata::GTFS::Type::Frequency;
  2         6  
  2         84  
21 2     2   912 use Opendata::GTFS::Type::Route;
  2         8  
  2         109  
22 2     2   1004 use Opendata::GTFS::Type::Shape;
  2         6  
  2         89  
23 2     2   974 use Opendata::GTFS::Type::Stop;
  2         6  
  2         87  
24 2     2   934 use Opendata::GTFS::Type::StopTime;
  2         5  
  2         86  
25 2     2   971 use Opendata::GTFS::Type::Transfer;
  2         4  
  2         83  
26 2     2   939 use Opendata::GTFS::Type::Trip;
  2         5  
  2         120  
27              
28 2     2   3060 class Opendata::GTFS::Feed using Moose {
  2     2   49  
  2     2   14  
  2         4  
  2         119  
  2         9  
  2         2  
  2         16  
  2         537  
  2         3  
  2         13  
  2         102  
  2         4  
  2         89  
  2         8  
  2         4  
  2         151  
  2         63  
  2         11  
  2         4  
  2         16  
  2         6009  
  2         4  
  2         16  
  2         10588  
  2         3  
  2         17  
  2         6341  
  2         3  
  2         22  
  2         139  
  2         4  
  2         17  
  2         490  
  2         4  
  2         15  
  2         1926  
  2         4  
  2         14  
  2         8890  
  2         6  
  2         94  
  2         10  
  2         4  
  2         91  
  2         12  
  2         5  
  2         103  
  2         8  
  2         10  
  2         283  
  2         19  
  2         7640  
  2         41  
  2         267  
29              
30 2         26 our $VERSION = '0.0102'; # VERSION
31              
32 2     2   21298 use Path::Tiny;
  2         5  
  2         127  
33 2     2   1261 use MooseX::AttributeDocumented;
  2         12124  
  2         9  
34              
35 2         10 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         10332 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         6334 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         6309 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   192527 fun type_to_singular($type) {
  2 50   94   3  
  2         339  
  2         113  
  47         270  
  47         20069  
  2         22  
76 2         9 my $name = $type->name;
77 2         7 $name =~ s{(?<=[a-z])([A-Z])}{_$1}g;
78 2         46 return lc $name;
79             }
80 2 50   2   2057 fun type_to_plural($type) {
  2 50   47   2  
  2         578  
  2         19  
  2         69  
  2         4  
  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         76 my $type = $attributes[$i];
88 94         208 my $attribute = type_to_plural($type);
89 94         215 my $singular = type_to_singular($type);
90              
91             has $attribute => (
92             is => 'ro',
93             isa => ArrayRef[ $type ],
94             traits => ['Array'],
95 94         361 default => sub { [] },
96 94         110 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   4220 around BUILDARGS($orig: $self, @args) {
  2 50   2   16  
  2 50       1185  
  94 50       926  
  2         304  
  0         0  
  0         0  
  0         0  
  0         0  
  0         0  
107 1         84 my %args = @args;
108 1 50       8 if(!exists $args{'directory'}) {
109 1         5 $args{'directory'} = File::Temp->newdir;
110             }
111 1         279979 $args{'directory'} = path($args{'directory'})->absolute;
112 1 50       378 if(exists $args{'file'}) {
    100          
113 1 0       3 if(path($args{'file'})->exists) {
114 1         8 $args{'file'} = path($args{'file'})->absolute;
115 1         7 my $x = Archive::Extract->new(archive => $args{'file'}->stringify);
116 1 0       9 $x->extract(to => $args{'directory'}->stringify) or die $x->error;
117             }
118             else {
119 1         549 die sprintf 'Supplied filepath (%s) does not exist.', $args{'file'};
120             }
121             }
122             elsif(exists $args{'url'}) {
123 1     1   253 eval "use HTTP::Tiny";
124 2 50       121260 die "Passing 'url' to Opendata::GTFS::Feed->new requires HTTP::Tiny" if $@;
125              
126 2         12 my $response = HTTP::Tiny->new->get($args{'url'});
127              
128 2 50       3 die sprintf "Can't download %s: %s", $args{'url'}, join (' - ' => $response->{'status'}, $response->{'reason'}) if !$response->{'success'};
129              
130 2         16 my $filename = $args{'url'};
131 24         53 $filename =~ s{/?\?.*}{};
132 24         41 $filename =~ s{.*/([^/]*)$}{$1};
133 24 50       60 $filename .= '.zip' if index ($filename, '.') == -1;
134              
135 24         829 $args{'directory'}->child($filename)->spew($response->{'content'});
136              
137 1         95 my $x = Archive::Extract->new(archive => $args{'directory'}->child($filename)->stringify);
138 23 50       1598 $x->extract(to => $args{'directory'}->stringify) or die $x->error;
139             }
140 23         67 $self->$orig(%args);
141             }
142              
143 2 50   2   2002 method BUILD {
  2     2   5  
  2         415  
  94         415  
  23         147  
  23         63  
144             FILE:
145 23         50 for (my $i = 0; $i < $#attributes; $i += 3) {
146 23         54 my $type = $attributes[$i];
147 23         48 my $is_required = $attributes[$i + 1];
148 23         27 my $filename = $attributes[$i + 2];
149              
150 23 100       49 if(!$self->directory->child($filename)->exists) {
151 23 50       61 next FILE if !$is_required;
152             }
153 23         263 $self->parse_file($type, $filename);
154              
155 23         2480 my $plural = type_to_plural($type);
156 23         730 my $method = "count_$plural";
157             }
158             }
159              
160 2 50   2   3263 method parse_file($type, $filename) {
  2 50   23   4  
  2 50       1118  
  47 50       163  
  23         4635  
  23         859  
  0         0  
  23         26  
  23         67  
161 23         101 my $method = sprintf 'add_%s', type_to_singular($type);
162 17         64 my $class = sprintf 'Opendata::GTFS::Type::%s', $type->name;
163              
164 1         8 my $csv = Text::CSV->new( { binary => 1 } );
165 8         7 my $fh;
166 1         9 File::BOM::open_bom($fh, $self->directory->child($filename), ':utf8');
167              
168 47     2   158 my $column_names = $csv->getline($fh);
  47         74  
  47         142  
  23         127  
169 153 50       3993 if(!defined $column_names) {
170 153         4811 die sprintf "Can't read the first line of the file. Check %s for errors.", $self->directory->child($filename);
171             }
172 152         838 my @column_names = @{ $column_names };
  152         114  
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 100 100 17   304 if($type->name eq 'StopTime' && any { $_ eq 'drop_off_time' } @column_names) {
  152         177  
177 152     8   978 my $index = first_index { $_ eq 'drop_off_time'} @column_names;
  152         5289  
178              
179 152 50       2144 $column_names[ $index ] = 'drop_off_type' if $index >= 0;
180             }
181              
182             LINE:
183 23         616 while(1) {
184 1         7 my $line = $csv->getline($fh);
185 1 100 100     2 last LINE if $csv->eof && !defined $line;
186 1 50       13 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.0102, released 2015-02-19.
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