Opendata-GTFS-Feed
view release on metacpan or search on metacpan
lib/Opendata/GTFS/Feed.pm view on Meta::CPAN
use 5.10.0;
use strict;
use warnings;
package Opendata::GTFS::Feed;
# ABSTRACT: Parse General Transit Feeds (GTFS)
our $AUTHORITY = 'cpan:CSSON'; # AUTHORITY
our $VERSION = '0.0202';
use Opendata::GTFS::Feed::Elk;
use Archive::Extract;
use File::Temp;
use Text::CSV;
use Lingua::EN::Inflect;
use File::BOM;
use Opendata::GTFS::Type::Agency;
use Opendata::GTFS::Type::Calendar;
use Opendata::GTFS::Type::CalendarDate;
use Opendata::GTFS::Type::FareAttribute;
use Opendata::GTFS::Type::FareRule;
use Opendata::GTFS::Type::Frequency;
use Opendata::GTFS::Type::Route;
use Opendata::GTFS::Type::Shape;
use Opendata::GTFS::Type::Stop;
use Opendata::GTFS::Type::StopTime;
use Opendata::GTFS::Type::Transfer;
use Opendata::GTFS::Type::Trip;
use Types::Opendata::GTFS -types;
use List::UtilsBy qw/zip_by/;
use List::Util qw/any/;
use Path::Tiny;
use MooseX::AttributeDocumented;
use Types::Path::Tiny qw/AbsPath/;
use Types::URI qw/Uri/;
use Types::Standard qw/ArrayRef/;
has file => (
is => 'ro',
isa => AbsPath,
required => 0,
coerce => 1,
documentation_order => 1,
documentation => q{If file is given, the feed in the file will be parsed.},
);
has directory => (
is => 'ro',
isa => AbsPath,
required => 0,
coerce => 1,
documentation_order => 3,
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).},
);
has url => (
is => 'ro',
isa => Uri,
required => 0,
coerce => 1,
documentation_order => 1,
documentation => q{If url is given, the feed at the url will be fetched and parsed.},
);
my @attributes = (
Agency, 1 => 'agency.txt',
Stop, 1 => 'stops.txt',
Route, 1 => 'routes.txt',
Trip, 1 => 'trips.txt',
StopTime, 1 => 'stop_times.txt',
Calendar, 1 => 'calendar.txt',
CalendarDate, 0 => 'calendar_dates.txt',
FareAttribute, 0 => 'fare_attributes.txt',
FareRule, 0 => 'fare_rules.txt',
Shape, 0 => 'shapes.txt',
Frequency, 0 => 'frequencies.txt',
lib/Opendata/GTFS/Feed.pm view on Meta::CPAN
if(exists $args{'file'}) {
if(path($args{'file'})->exists) {
$args{'file'} = path($args{'file'})->absolute;
my $x = Archive::Extract->new(archive => $args{'file'}->stringify);
$x->extract(to => $args{'directory'}->stringify) or die $x->error;
}
else {
die sprintf 'Supplied filepath (%s) does not exist.', $args{'file'};
}
}
elsif(exists $args{'url'}) {
eval "use HTTP::Tiny";
die "Passing 'url' to Opendata::GTFS::Feed->new requires HTTP::Tiny" if $@;
my $response = HTTP::Tiny->new->get($args{'url'});
die sprintf "Can't download %s: %s", $args{'url'}, join (' - ' => $response->{'status'}, $response->{'reason'}) if !$response->{'success'};
my $filename = $args{'url'};
$filename =~ s{/?\?.*}{};
$filename =~ s{.*/([^/]*)$}{$1};
$filename .= '.zip' if index ($filename, '.') == -1;
$args{'directory'}->child($filename)->spew($response->{'content'});
my $x = Archive::Extract->new(archive => $args{'directory'}->child($filename)->stringify);
$x->extract(to => $args{'directory'}->stringify) or die $x->error;
}
$self->$orig(%args);
};
sub BUILD {
my $self = shift;
FILE:
for (my $i = 0; $i < $#attributes; $i += 3) {
my $type = $attributes[$i];
my $is_required = $attributes[$i + 1];
my $filename = $attributes[$i + 2];
if(!$self->directory->child($filename)->exists) {
next FILE if !$is_required;
}
$self->parse_file($type, $filename);
my $plural = type_to_plural($type);
my $method = "count_$plural";
}
}
sub parse_file {
my $self = shift;
my $type = shift;
my $filename = shift;
my $method = sprintf 'add_%s', type_to_singular($type);
my $class = sprintf 'Opendata::GTFS::Type::%s', $type->name;
my $csv = Text::CSV->new( { binary => 1 } );
my $fh;
File::BOM::open_bom($fh, $self->directory->child($filename), ':utf8');
my $column_names = $csv->getline($fh);
if(!defined $column_names) {
die sprintf "Can't read the first line of the file. Check %s for errors.", $self->directory->child($filename);
}
my @column_names = @{ $column_names };
# Google's example feed (https://developers.google.com/transit/gtfs/examples/gtfs-feed / https://developers.google.com/transit/gtfs/examples/sample-feed.zip)
# has a (reported) bug. This fixes that.
if($type->name eq 'StopTime' && any { $_ eq 'drop_off_time' } @column_names) {
my $index = first_index { $_ eq 'drop_off_time'} @column_names;
$column_names[ $index ] = 'drop_off_type' if $index >= 0;
}
LINE:
while(1) {
my $line = $csv->getline($fh);
last LINE if $csv->eof && !defined $line;
next LINE if !defined $line;
next LINE if scalar @{ $line } == 1 && (!defined $line->[0] || $line->[0] eq ''); # skip empty lines
my @args = zip_by { @_ } \@column_names, $line;
$self->$method($class->new(@args));
last LINE if $csv->eof;
}
close $fh or die sprintf "Can't close %s", $self->directory->child($filename);
}
__PACKAGE__->meta->make_immutable;
1;
__END__
=pod
=encoding utf-8
=head1 NAME
Opendata::GTFS::Feed - Parse General Transit Feeds (GTFS)
=begin html
<p>
<img src="https://img.shields.io/badge/perl-5.10+-blue.svg" alt="Requires Perl 5.10+" />
<a href="https://travis-ci.org/Csson/p5-Opendata-GTFS-Feed"><img src="https://api.travis-ci.org/Csson/p5-Opendata-GTFS-Feed.svg?branch=master" alt="Travis status" /></a>
<a href="http://cpants.cpanauthors.org/dist/Opendata-GTFS-Feed-0.0202"><img src="https://badgedepot.code301.com/badge/kwalitee/Opendata-GTFS-Feed/0.0202" alt="Distribution kwalitee" /></a>
<a href="http://matrix.cpantesters.org/?dist=Opendata-GTFS-Feed%200.0202"><img src="https://badgedepot.code301.com/badge/cpantesters/Opendata-GTFS-Feed/0.0202" alt="CPAN Testers result" /></a>
<img src="https://img.shields.io/badge/coverage-95.2%-yellow.svg" alt="coverage 95.2%" />
</p>
=end html
=head1 VERSION
( run in 1.337 second using v1.01-cache-2.11-cpan-b16cb0d3907 )