#!/usr/bin/env perl
use strict;
use warnings;
use 5.010;
no if $] >= 5.018, warnings => "experimental::smartmatch";
our $VERSION = '1.09';
binmode( STDOUT, ':encoding(utf-8)' );
use Encode qw(decode);
use Getopt::Long qw(:config no_ignore_case);
use List::Util qw(max);
use Travel::Status::DE::EFA;
my $efa_url = 'http://efa.vrr.de/vrr/XSLT_DM_REQUEST';
my ( $date, $time, $input_type, $list_lines, $offset, $relative_times );
my ($timeout);
my ( @grep_lines, @grep_platforms );
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
GetOptions(
'd|date=s' => \$date,
'h|help' => sub { show_help(0) },
'l|line=s@' => \@grep_lines,
'L|linelist' => \$list_lines,
'o|offset=i' => \$offset,
'p|platform=s@' => \@grep_platforms,
'r|relative' => \$relative_times,
't|time=s' => \$time,
'timeout=i' => \$timeout,
'u|efa-url=s' => \$efa_url,
'V|version' => \&show_version,
) or show_help(1);
if ( @ARGV != 2 ) {
show_help(1);
}
# --line=foo,bar support
@grep_lines = split( qr{,}, join( q{,}, @grep_lines ) );
@grep_platforms = split( qr{,}, join( q{,}, @grep_platforms ) );
my ( $place, $input ) = @ARGV;
if ( $input =~ s{ ^ (?<type> address|poi|stop) : }{}x ) {
$input_type = $+{type};
}
my $status = Travel::Status::DE::EFA->new(
date => $date,
efa_url => $efa_url,
place => $place,
name => $input,
time => $time,
type => $input_type,
timeout => $timeout,
);
sub show_help {
my ($code) = @_;
print "Usage: efa-m [-d <dd.mm.yyyy>] [-t <hh:mm>] <city> <station>\n"
. "See also: man efa-m\n";
exit $code;
}
sub show_version {
say "efa-m version ${VERSION}";
exit 0;
}
sub display_result {
my (@lines) = @_;
my @line_length;
if ( not @lines ) {
die("Nothing to show\n");
}
for my $i ( 0 .. 3 ) {
$line_length[$i] = max map { length( $_->[$i] ) } @lines;
}
for my $line (@lines) {
if ( length( $line->[4] ) ) {
$line->[4] =~ tr{\n\x0d}{ }s;
chomp $line->[4];
print "\n";
for my $info_line ( split( qr{\n}, $line->[4] ) ) {
say "# ${info_line}";
}
}
printf(
join( q{ }, ( map { "%-${_}s" } @line_length ) ) . "\n",
@{$line}[ 0 .. 3 ]
);
}
return;
}
sub show_lines {
my @output;
for my $l ( $status->lines ) {
if ( @grep_lines and not( $l->name ~~ \@grep_lines ) ) {
next;
}
push( @output,
[ $l->type, $l->name, $l->direction // q{}, $l->route // q{} ] );
}
display_result(@output);
return;
}
sub show_results {
my @output;
for my $d ( $status->results ) {
my $platform = $d->platform;
my $dtime = (
$relative_times ? sprintf( '%2d min', $d->countdown ) : $d->time );
if ( $d->platform_db ) {
$platform .= ' (DB)';
}
if (
( @grep_lines and not( $d->line ~~ \@grep_lines ) )
or ( @grep_platforms
and not( $platform ~~ \@grep_platforms ) )
or ( $offset and $d->countdown < $offset )
)
{
next;
}
if ( $d->is_cancelled ) {
if ($relative_times) {
next;
}
else {
$dtime .= ' CANCELED';
}
}
if ( $d->delay ) {
$dtime .= ' (+' . $d->delay . ')';
}
push( @output,
[ $dtime, $platform, $d->line, $d->destination, $d->info ] );
}
display_result(@output);
return;
}
if ( my $err = $status->errstr ) {
say STDERR "Request error: ${err}";
exit 2;
}
if ($list_lines) {
show_lines();
}
else {
show_results();
}
__END__
=head1 NAME
efa-m - Unofficial interface to the efa.vrr.de departure monitor
=head1 SYNOPSIS
B<efa-m> [B<-Lr>] [B<-d> I<dd.mm.yyyy>] [B<-t> I<hh:mm>]
[B<-l> I<lines>] [B<-p> I<platforms>] [B<-u> I<url>]
I<city> [I<type>B<:>]I<name>
=head1 VERSION
version 1.09
=head1 DESCRIPTION
B<efa-m> lists upcoming tram, bus and train departures at the location I<name>
in I<city>.
By default, I<name> refers to a stop, this can be changed by specifying
I<type>. Supported types are B<address> and B<poi> (point of interest).
=head1 OPTIONS
=over
=item B<-d>, B<--date> I<dd.mm.yyyy>
Show departures for I<date> instead of today.
May also be specified as I<dd.mm.>
=item B<-L>, B<--linelist>
Do not show departures. Instead, list all lines serving the specified place.
Note that this information may be incomplete -- only lines which are in
service either at the time of the B<efa-m> call or at the time specifed
using B<--date> and B<--time> are guaranteed to be included.
=item B<-l>, B<--line> I<lines>
Only show departures of I<lines> (comma-separatad list, option may be
repeated)
=item B<-o>, B<--offset> I<minutes>
Ignore departures which are less than I<minutes> from now.
=item B<-p>, B<--platform> I<platforms>
Only show departures at I<platforms> (comma-separated list, option may be
repeated). Note that the C<< Bstg. >> / C<< Gleis >> prefix must be omitted.
=item B<-r>, B<--relative>
Use relative departure times.
=item B<-t>, B<--time> I<hh:mm>
Show departures starting at I<time> instead of now.
=item B<-u>, B<--efa-url> I<url>
URL to the EFA entry point, defaults to L<http://efa.vrr.de/vrr/XSLT_DM_REQUEST>.
Depending on your location, some I<url>s may contain more specific data
than others. See Travel::Status::DE::EFA(3pm) for alternatives.
=item B<--timeout> I<seconds>
Set timeout for HTTP requests. Default: 10 seconds. Set to 0 or a negative
value to disable it.
=item B<-V>, B<--version>
Show version information.
=back
=head1 EXIT STATUS
Normally zero. B<1> means B<efa-m> was called with invalid options,
B<2> indicates a request error from Travel::Status::DE::EFA(3pm).
=head1 CONFIGURATION
None.
=head1 DEPENDENCIES
=over
=item * Class::Accessor(3pm)
=item * LWP::UserAgent(3pm)
=item * Travel::Status::DE::EFA(3pm)
=item * XML::LibXML(3pm)
=back
=head1 BUGS AND LIMITATIONS
B<efa-m> uses the VRR EFA service by default, which seems to contain the
greatest available set of information. However, some cities (e.g. Berlin
or parts of Hamburg) are incomplete. B<efa-m> is not yet able to choose the
appropriate EFA URL for these by itself, it needs to be done manually with
the B<-u>, B<--efa-url> option.
=head1 AUTHOR
Copyright (C) 2011-2014 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt>
=head1 LICENSE
This program is licensed under the same terms as Perl itself.