#!/usr/bin/env perl
## Copyright © 2009-2014 by Daniel Friesel <derf@finalrewind.org>
## License: WTFPL <http://sam.zoy.org/wtfpl>
## 0. You just DO WHAT THE FUCK YOU WANT TO.
use strict;
use warnings;
use 5.010;
no if $] >= 5.018, warnings => "experimental::smartmatch";
use utf8;
use Encode qw(decode);
use Travel::Routing::DE::EFA;
use Exception::Class;
use Getopt::Long qw/:config no_ignore_case/;
use List::Util qw(first);
our $VERSION = '2.08';
my $ignore_info = 'Fahrradmitnahme';
my $efa;
my $efa_url = 'http://efa.vrr.de/vrr/XSLT_TRIP_REQUEST2';
my ( @from, @to, @via, $from_type, $to_type, $via_type );
my $opt = {
'efa-url' => \$efa_url,
'help' => sub { show_help(0) },
'ignore-info' => \$ignore_info,
'from' => \@from,
'to' => \@to,
'version' => sub { say "efa version $VERSION"; exit 0 },
'via' => \@via,
};
binmode( STDOUT, ':encoding(utf-8)' );
binmode( STDERR, ':encoding(utf-8)' );
sub show_help {
my ($exit_status) = @_;
say 'Usage: efa [options] <from-city> <from-stop> <to-city> <to-stop>';
say 'See also: man efa';
exit $exit_status;
}
sub handle_efa_exception {
my ($e) = @_;
if ( $e->isa('Travel::Routing::DE::EFA::Exception::Setup') ) {
if ( $e->message ) {
printf STDERR (
"Error: %s (option '%s'): %s\n",
$e->description, $e->option, $e->message
);
}
else {
printf STDERR (
"Error: %s (option '%s', got '%s', want '%s')\n",
$e->description, $e->option, $e->have, $e->want
);
}
exit 1;
}
if ( $e->isa('Travel::Routing::DE::EFA::Exception::Net') ) {
printf STDERR ( "Error: %s: %s\n", $e->description,
$e->http_response->as_string );
exit 2;
}
if ( $e->isa('Travel::Routing::DE::EFA::Exception::NoData') ) {
printf STDERR ( "Error: %s\n", $e->description );
exit 3;
}
if ( $e->isa('Travel::Routing::DE::EFA::Exception::Ambiguous') ) {
printf STDERR (
"Error: %s for key %s. Specify one of %s\n",
$e->description, $e->post_key, $e->possibilities
);
exit 4;
}
if ( $e->isa('Travel::Routing::DE::EFA::Exception::Other') ) {
printf STDERR ( "Error: %s: %s\n", $e->description, $e->message );
exit 5;
}
printf STDERR ( "Uncaught exception: %s\n%s", ref($e), $e->trace );
exit 10;
}
sub check_for_error {
my ($eval_error) = @_;
if ( not defined $efa ) {
if ( $eval_error
and ref($eval_error)
and $eval_error->isa('Travel::Routing::DE::EFA::Exception') )
{
handle_efa_exception($eval_error);
}
elsif ($eval_error) {
printf STDERR
"Unknown Travel::Routing::DE::EFA error:\n${eval_error}";
exit 10;
}
else {
say STDERR 'Travel::Routing::DE::EFA failed to return an object';
exit 10;
}
}
return;
}
sub display_connection {
my ($c) = @_;
if ( $c->delay ) {
printf( "# +%d, scheduled: %s -> %s\n",
$c->delay, $c->departure_stime, $c->arrival_stime );
}
for my $extra ( $c->extra ) {
if ( not( length $ignore_info and $extra =~ /$ignore_info/i ) ) {
say "# $extra";
}
}
if ( $opt->{maps} ) {
for my $m ( $c->departure_routemaps, $c->departure_stationmaps ) {
say "# $m";
}
}
printf(
"%-5s ab %-30s %-20s %s\n",
$c->departure_time, $c->departure_stop_and_platform,
$c->train_line, $c->train_destination,
);
if ( $opt->{'full-route'} ) {
for my $via_stop ( $c->via ) {
printf( "%-5s %-30s %s\n",
$via_stop->[1], $via_stop->[2], $via_stop->[3] );
}
}
printf( "%-5s an %s\n", $c->arrival_time, $c->arrival_stop_and_platform, );
print "\n";
return;
}
@ARGV = map { decode( 'UTF-8', $_ ) } @ARGV;
#<<<
GetOptions(
$opt,
qw{
arrive|a=s
auto-url|discover-and-print|A
bike|b
date|d=s
depart=s
discover|D
efa-url|u=s
exclude|e=s@
extended-info|E
from=s@{2}
full-route|f
help|h
ignore-info|I:s
include|i=s
list|l
maps|M
max-change|m=i
num-connections|n=i
prefer|P=s
proximity|p
service|s=s
time|t=s
timeout=i
to=s@{2}
version|v
via=s@{2}
walk-speed|w=s
},
) or show_help(1);
#>>>
if ( $opt->{list} ) {
printf( "%-40s %-14s %s\n\n", 'service', 'abbr. (-s)', 'url (-u)' );
for my $service ( Travel::Routing::DE::EFA::get_efa_urls() ) {
printf( "%-40s %-14s %s\n", @{$service}{qw(name shortname url)} );
}
exit 0;
}
if ( not( @from and @to ) ) {
if ( @ARGV == 4 ) {
( @from[ 0, 1 ], @to[ 0, 1 ] ) = @ARGV;
}
elsif ( @ARGV == 6 ) {
( @from[ 0, 1 ], @via[ 0, 1 ], @to[ 0, 1 ] ) = @ARGV;
}
else {
show_help(1);
}
}
for my $pair ( [ \@from, \$from_type ], [ \@via, \$via_type ],
[ \@to, \$to_type ], )
{
next if ( not defined $pair->[0]->[1] );
if (
$pair->[0]->[1] =~ s{ ^ (?<type> [^:]+ ) : \s* (?<target> .+ ) $ }
{$+{target}}x
)
{
given ( $+{type} ) {
when ('addr') { ${ $pair->[1] } = 'address' }
default { ${ $pair->[1] } = $+{type} }
}
}
}
if ( defined $opt->{'ignore-info'} and length( $opt->{'ignore-info'} ) == 0 ) {
$opt->{'ignore-info'} = undef;
}
if ( $opt->{exclude} ) {
$opt->{exclude} = [ split( /,/, join( ',', @{ $opt->{exclude} } ) ) ];
}
if ( $opt->{service} ) {
my $service = first { lc($_->{shortname}) eq lc($opt->{service}) }
Travel::Routing::DE::EFA::get_efa_urls();
if ( not $service ) {
printf STDERR (
"Error: Unknown service '%s'. See 'efa -l' for a "
. "list of supported service names\n",
$opt->{service}
);
exit 1;
}
$efa_url = $service->{url};
}
if ( $opt->{discover} or $opt->{'auto-url'} ) {
for my $service ( Travel::Routing::DE::EFA::get_efa_urls() ) {
$efa = eval {
Travel::Routing::DE::EFA->new(
efa_url => $service->{url},
origin => [ @from, $from_type ],
destination => [ @to, $to_type ],
via => ( @via ? [ @via, $via_type ] : undef ),
arrival_time => $opt->{arrive},
departure_time => $opt->{depart} // $opt->{time},
date => $opt->{date},
exclude => $opt->{exclude},
train_type => $opt->{include},
with_bike => $opt->{bike},
select_interchange_by => $opt->{prefer},
use_near_stops => $opt->{proximity},
walk_speed => $opt->{'walk-speed'},
max_interchanges => $opt->{'max-change'},
num_results => $opt->{'num-connections'},
lwp_options => { timeout => $opt->{timeout} },
);
};
if ($efa) {
if ( $opt->{'auto-url'} ) {
last;
}
printf(
"%s / %s (%s)\n -> efa -s %s %s\n\n",
@{$service}{qw(name shortname url shortname)},
join( ' ', map { "'$_'" } @ARGV ),
);
}
}
if ( $opt->{'discover'} ) {
exit 0;
}
}
else {
$efa = eval {
Travel::Routing::DE::EFA->new(
efa_url => $efa_url,
origin => [ @from, $from_type ],
destination => [ @to, $to_type ],
via => ( @via ? [ @via, $via_type ] : undef ),
arrival_time => $opt->{arrive},
departure_time => $opt->{depart} // $opt->{time},
date => $opt->{date},
exclude => $opt->{exclude},
train_type => $opt->{include},
with_bike => $opt->{bike},
select_interchange_by => $opt->{prefer},
use_near_stops => $opt->{proximity},
walk_speed => $opt->{'walk-speed'},
max_interchanges => $opt->{'max-change'},
num_results => $opt->{'num-connections'},
lwp_options => { timeout => $opt->{timeout} },
);
};
}
check_for_error($@);
my @routes = $efa->routes;
for my $i ( 0 .. $#routes ) {
my $route = $routes[$i];
if ( $opt->{'extended-info'} ) {
print '# ' . $route->duration;
if ( $route->ticket_type ) {
printf( ", class %s (%s€ / %s€)\n\n",
$route->ticket_type, $route->fare_adult, $route->fare_child, );
}
else {
print "\n\n";
}
}
for my $c ( $route->parts ) {
display_connection($c);
}
# last one needs to be shown separately
if ( $opt->{maps} ) {
my $c = ( $route->parts )[-1];
for my $m ( $c->arrival_routemaps, $c->arrival_stationmaps ) {
say "# $m";
}
}
if ( $i != $#routes ) {
print "---------\n\n";
}
}
__END__
=head1 NAME
efa - unofficial efa.vrr.de command line client
=head1 SYNOPSIS
=over
=item B<efa> B<--from> I<city> I<stop> B<--to> I<city> I<stop> [ I<additional options> ]
=item B<efa> [ I<options> ] I<from-city> I<from-stop> [ I<via-city> I<via-stop> ] I<to-city> I<to-stop>
=back
=head1 VERSION
version 2.08
=head1 DESCRIPTION
B<efa> is a command line client for the L<http://efa.vrr.de> web interface.
It sends the specified information to the online form and displays the results.
It also supports other EFA services than L<http://efa.vrr.de>. B<efa> has a
builtin list of EFA entry points which can be probed with the B<-A> and B<-D>
options and listed with B<-l>. You can also specify a custom service using
B<-u> I<url> or B<-s> I<name>.
However, the default EFA service is sufficient in most cases (even ICE
connections all over Germany).
=head1 OPTIONS
=over
=item B<--from> I<city> I<stop>
Departure place
=item B<--to> I<city> I<stop>
Arrival place
=item B<--via> I<city> I<stop>
Travel via this place
In all cases, if you want I<stop> to be an address or "point of interest", you
can set it to 'addr:something' or 'poi:something'.
=item B<-a>|B<--arrive> I<hh>:I<mm>
Journey end time (overrides --time/--depart)
=item B<-A>|B<--auto-url>|B<--discover-and-print>
Probe all known EFA entry points for the specified connection. Print the first
result which was not an error.
Note that this may take a while and will not necessarily return the best
result. Also, using this option by default is not recommended, as it puts
EFA services under considerable additional load.
=item B<-b>|B<--bike>
Choose connections allowing to carry a bike
=item B<-d>|B<--date> I<dd>.I<mm>.[I<yyyy>]
Journey date
=item B<-D>|B<--discover>
Probe all known EFA entry points for the specified connection. No routes are
returned in this case. Instead, B<efa> will print the URLs and names of all
entry points which did not return an error.
=item B<-u>|B<--efa-url> I<url>
URL to the EFA entry point, defaults to
L<http://efa.vrr.de/vrr/XSLT_TRIP_REQUEST2>. Depending on your location, some
I<url>s may contain more specific data than others. See
Travel::Routing::DE::EFA(3pm) and the B<-l> option for alternatives.
=item B<-e>|B<--exclude> I<transports>
Exclude I<transports> (comma separated list).
Possible transports: zug, s-bahn, u-bahn, stadtbahn, tram, stadtbus, regionalbus,
schnellbus, seilbahn, schiff, ast, sonstige
=item B<-E>|B<--extended-info>
Display duration, ticket class and price for each route (if available)
=item B<-f>|B<--full-route>
Display intermediate stops (with time and platform) of each train. Note that
these are not always available.
=item B<-I>|B<--ignore-info> [ I<regex> ]
Ignore additional information matching I<regex> (default: /Fahrradmitnahme/)
If I<regex> is not supplied, removes the default regex (-E<gt> nothing will be ignored)
=item B<-i>|B<--include> I<type>
Include connections using trains of type I<type>, where I<type> may be:
=over
=item * local (default)
only take local trains ("Verbund-/Nahverkehrslinien"). Slow, but the cheapest
method if you're not traveling long distance
=item * ic
Local trains + IC
=item * ice
All trains (local + IC + ICE)
=back
=item B<-l>|B<--list>
List supported EFA services wit their URLs (see B<-u>) and abbreviations
(see B<-s>).
=item B<-M>|B<--maps>
Output links to maps of transfer paths and transfer stations where
available.
=item B<-m>|B<--max-change> I<number>
Print connections with at most I<number> interchanges
=item B<-n>|B<--num-connections> I<number>
Return up to I<number> connections. If unset, the default of the respective
EFA server is used (usually 4 or 5).
=item B<-P>|B<--prefer> I<type>
Prefer connections of I<type>:
=over
=item * speed (default)
The faster, the better
=item * nowait
Prefer connections with less interchanges
=item * nowalk
Prefer connections with less walking (at interchanges)
=back
=item B<-p>|B<--proximity>
Take stops close to the stop/start into account and possibly use them instead
=item B<-s>|B<--service> I<name>
Shortname of the EFA entry point. See Travel::Routing::DE::EFA(3pm) and
the B<-l> option for a list of services.
=item B<-t>|B<--time>|B<--depart> I<hh>:I<mm>
Journey start time
=item B<--timeout> I<seconds>
Set timeout for HTTP requests. Default: 60 seconds.
=item B<-v>|B<--version>
Print version information
=item B<-w>|B<--walk-speed> I<speed>
Set your walking speed to I<speed>.
Accepted values: normal (default), fast, slow
=back
=head1 EXIT STATUS
0 Everything went well
1 Invalid arguments, see error message
2 Network error, unable to send request
3 efa.vrr.de did not return any parsable data
4 efa.vrr.de error: ambiguous input
5 efa.vrr.de error: no connections found
10 Unknown Travel::Routing::DE::EFA error
255 Other internal error
=head1 CONFIGURATION
None.
=head1 EXAMPLES
=over
=item efa Do Hbf MH Hbf
Look up a connection from Dortmund (Do) Hbf to ME<uuml>lheim (MH) Hbf
=item efa --include ice Essen Hbf Hamburg Dammtor
Look up a connection with long-distance trains
=item efa --arrive 18:00 -e zug,s-bahn -M E Wickenburgstr D Oststr
Look up a connection from Essen Wickenburgstr to DE<uuml>sseldorf Oststr.
Do not use any trains, make sure to arrive around 18:00 and print links to
maps of all interchange stations.
=item efa -s vvs Stuttgart Hbf Stuttgart Marienplatz
Use the VVS (Verkehrsverbund Stuttgart) EFA service to look up a connection.
=back
=head1 DEPENDENCIES
This script requires perl 5.10 (or higher) with the following modules:
=over
=item * Class::Accessor
=item * Exception::Class
=item * LWP::UserAgent
=item * XML::LibXML
=back
=head1 BUGS AND LIMITATIONS
The EFA backend is not able to calculate "two-way" routes, i.e. from -> via ->
to routes with from == to. If from and to are the same stop, it doesn't even
try to calculate a route ("we recommend walking instead"), if they are close to
each other it may or may not work. Workaround: Request from -> via using the
normal four-argument efa invocation, read the time, use efa -t time via via to
to to request via -> to.
=head1 AUTHOR
Copyright (C) 2009-2014 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt>
=head1 LICENSE
0. You just DO WHAT THE FUCK YOU WANT TO.