#!/usr/bin/env perl ## Copyright © 2009 by Daniel Friesel <derf@derf.homelinux.org> ## License: WTFPL <http://sam.zoy.org/wtfpl> use strict; use warnings; use 5.010; use encoding 'utf8'; use Getopt::Long; use WWW::Mechanize; my $firsturl = 'http://efa.vrr.de/vrr/XSLT_TRIP_REQUEST2?language=de&itdLPxx_transpCompany=vrr'; my $posturl = 'http://efa.vrr.de/vrr/XSLT_TRIP_REQUEST2'; my $content; my %post; my $www = WWW::Mechanize->new( autocheck => 1, ); my $raw; my $cons; my (@from, @to, @via); my ($from_type, $to_type, $via_type) = ('stop', 'stop', 'stop'); my ($time, $time_depart, $time_arrive); my $date; my @exclude; my $maxinter; my $restrict; my $prefer; my $proximity; my $walk_speed; my $with_bike; my $debug = 0; my $ignore_info = 'Fahrradmitnahme'; my ($i, $j, $con, $part); sub check_ambiguous($) { my $html = shift; my $choose_re = qr/<span class="errorTextBold">Bitte auswählen<\/span>/; my $select_re = qr/<select name="(?<what>(place|type|name)_(origin|destination))"/; my $option_re = qr/<option value="\d+(:\d+)*"( selected)?>(?<choice>[^<]+)<\/option>/; if ($html =~ /$choose_re/s) { foreach (split(/$choose_re/s, $html)) { if (/$select_re/) { print "Ambiguous input for $+{what}\n"; } while (/$option_re/gs) { print "\t$+{choice}\n"; } } return(1); } return(0); } sub parse_content($) { my $raw = shift; my $groupsize = 8; my $offset; my $return; my @extra; for (my $offer = 0; exists($raw->[$offer]); $offer++) { foreach (@{$raw->[$offer]}) { s/\s*<br>\s*/, /g; s/<[^>]+>//g; } for (my $i = 0; @{$raw->[$offer]} >= (($i+1) * $groupsize) - 1; $i++) { $offset = $i * $groupsize; undef(@extra); if ($raw->[$offer]->[$offset+2] =~ /^Fußweg/) { # These are generic, which means they don't contain a time splice(@{$raw->[$offer]}, $offset, 0, ''); splice(@{$raw->[$offer]}, $offset+4, 0, ''); } if ($raw->[$offer]->[$offset+3] =~ /^Fußweg/) { # These messages lack the last element, so inject it splice(@{$raw->[$offer]}, $offset+7, 0, ''); } for my $j (0, 4, 8) { until (not exists($raw->[$offer]->[$offset+$j]) or $raw->[$offer]->[$offset+$j] =~ /^(\d+:\d+)?$/) { last unless (exists($raw->[$offer]->[$offset+$j])); last if ($raw->[$offer]->[$offset+$j] eq 'Verspätungen sind berücksichtigt'); if ($raw->[$offer]->[$offset+$j] =~ /^\s*$/) { splice(@{$raw->[$offer]}, $offset+$j, 1); } else { push(@extra, splice(@{$raw->[$offer]}, $offset+$j, 1)); } } } $return->[$offer]->[$i] = { deptime => $raw->[$offer]->[$offset], dep => $raw->[$offer]->[$offset+1], depstop => $raw->[$offer]->[$offset+2], deptrain => $raw->[$offer]->[$offset+3], depdest => $raw->[$offer]->[$offset+7], arrtime => $raw->[$offer]->[$offset+4], arr => $raw->[$offer]->[$offset+5], arrstop => $raw->[$offer]->[$offset+6], }; @{$return->[$offer]->[$i]->{extra}} = @extra; } } return($return); } sub prepare_content($) { my $html = shift; my $offer = 0; my $return; # beware of the no-break space (U+00A0) ↓ ↓ foreach (split(/<span class="labelTextBold"> \d+\. Fahrt<\/span>/, $html)) { unless ($offer) { $offer++; next; } foreach(split(/\n/)) { if (/<span class="labelText"( valign="center")?>(?<content>.+)<\/span><\/td>/) { push(@{$return->[$offer-1]}, $+{content}); } } $offer++; } return($return); } GetOptions( 'arrive=s' => \$time_arrive, 'bike' => \$with_bike, 'date=s' => \$date, 'debug' => \$debug, 'depart=s' => \$time_depart, 'exclude=s' => \@exclude, 'from=s{2}' => \@from, 'from-type=s' => \$from_type, 'help' => sub {exec('perldoc', $0)}, 'ignore-info=s{0,1}' => \$ignore_info, 'max-change=i' => \$maxinter, 'post=s' => \%post, 'prefer=s' => \$prefer, 'proximity' => \$proximity, 'restrict=s' => \$restrict, 'time=s' => \$time, 'to=s{2}' => \@to, 'to-type=s' => \$to_type, 'via=s{2}' => \@via, 'via-type=s' => \$via_type, 'walk-speed=s' => \$walk_speed, ); @exclude = split(/,/, join(',', @exclude)); unless (@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; } } unless (@to == 2 and @from == 2) { print STDERR "Usage: efa --from <city> <stop> --to <city> <stop> [other options]\n"; exit(1); } @post{'place_origin','name_origin'} = @from; @post{'place_destination','name_destination'} = @to; if (@via == 2) { @post{'place_via','name_via'} = @via; } foreach ($from_type, $to_type, $via_type) { unless ($_ ~~ ['stop', 'address', 'poi']) { $_ = 'stop'; print STDERR "from/to/via type: must be stop, address or poi\n"; } } $post{type_origin} = $from_type; $post{type_destination} = $to_type; $post{type_via} = $via_type; if ($time_arrive) { $time = $time_arrive; $post{itdTripDateTimeDepArr} = 'arr'; } elsif ($time_depart) { $time = $time_depart; $post{itdTripDateTimeDepArr} = 'dep'; } if ($time) { @post{'itdTimeHour','itdTimeMinute'} = split(/:/, $time); } if ($date) { @post{'itdDateDay','itdDateMonth','itdDateYear'} = split(/\./, $date); $post{itdDateYear} //= (localtime(time))[5] + 1900; } if (@exclude) { foreach(@exclude) { given($_) { when('zug') { $post{inclMOT_0} = undef } when('s-bahn') { $post{inclMOT_1} = undef } when('u-bahn') { $post{inclMOT_2} = undef } when('stadtbahn') { $post{inclMOT_3} = undef } when('tram') { $post{inclMOT_4} = undef } when('stadtbus') { $post{inclMOT_5} = undef } when('regionalbus') { $post{inclMOT_6} = undef } when('schnellbus') { $post{inclMOT_7} = undef } when('seilbahn') { $post{inclMOT_8} = undef } when('schiff') { $post{inclMOT_9} = undef } when('ast') { $post{inclMOT_10} = undef } when('sonstige') {$post{inclMOT_11} = undef } default { print STDERR "--exclude: invalid argument\n"; } } } } if (defined($maxinter)) { $post{maxChanges} = $maxinter; } if ($prefer) { given($prefer) { when('speed') { $post{routeType} = 'LEASTTIME' } when('nowait') { $post{routeType} = 'LEASTINTERCHANGE' } when('nowalk') { $post{routeType} = 'LEASTWALKING' } default { print STDERR "--prefer usage: speed / nowait / nowalk\n"; } } } if ($proximity) { $post{useProxFootSearch} = 1; } if ($restrict) { given ($restrict) { when('local') { $post{lineRestriction} = 403 } when('ic') { $post{lineRestriction} = 401 } when('ice') { $post{lineRestriction} = 400 } when(/\d+/) { $post{lineRestriction} = $restrict } default { print STDERR "--restrict usage: local / ic / ice\n"; } } } if ($walk_speed) { if ($walk_speed ~~ ['normal', 'fast', 'slow']) { $post{changeSpeed} = $walk_speed; } else { print STDERR "--walk-speed usage: normal / fast / slow\n"; } } if ($with_bike) { $ignore_info = undef; $post{bikeTakeAlong} = 1; } $www->get($firsturl); $www->submit_form( form_name => 'jp', fields => \%post, ); $content = $www->content; if (check_ambiguous($content)) { exit(1); } $raw = prepare_content($content); if ($debug) { print STDERR "custom post values used in query:\n"; foreach(keys(%post)) { print STDERR " $_ => $post{$_}\n"; } print STDERR "\nraw response:\n"; foreach(@$raw) { print STDERR "---\n"; foreach(@$_) { print STDERR "$_\n"; } } } $cons = parse_content($raw); for ($i = 0; $con = $cons->[$i]; $i++) { for ($j = 0; $part = $con->[$j]; $j++) { foreach (@{$part->{extra}}) { unless ($ignore_info and $_ =~ /$ignore_info/i) { print "# $_\n"; } } printf( "%-5s %-2s %-30s %-20s %s\n%-5s %-2s %-30s\n\n", $part->{deptime}, $part->{dep}, $part->{depstop}, $part->{deptrain}, $part->{depdest}, $part->{arrtime}, $part->{arr}, $part->{arrstop} ); } if (defined($cons->[$i+1])) { 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 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 =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 =item B<--from-type>, B<--to-type>, B<--via-type> I<type> Designate type of the I<stop> for from/to/via. Possible I<type>s: B<stop> (default), B<address>, B<poi> (point of interest) =item B<--time>|B<--depart> I<hh>:I<mm> Journey start time =item B<--arrive> I<hh>:I<mm> Journey end time (overrides --time/--depart) =item B<--date> I<dd>.I<mm>.[I<yyyy>] Journey date =item B<--bike> Choose connections where you can take a bike with you =item 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<--max-change> I<number> Print connections with at most I<number> interchanges =item 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<--proximity> Take stops close to the stop/start into account and possibly use them instead =item B<--restrict> I<type> Only accept 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 travelling long distance =item * ic All trains except ICE =item * ice All trains =back =item B<--walk-speed> I<speed> Set your walking speed to I<speed>. Accepted values: normal (default), fast, slow =item B<--ignore-info> [ I<regex> ] Ignore additional information matching I<regex> (default: /Fahrradmitnahme/). If I<regex> is not supplied, removes the default regex (-> nothing will be ignored) =item B<--debug> Display debug information (additional post requests sent to the site, raw items received from the site) =item B<--post> I<key>=I<value> Add I<key> with I<value> to the HTTP POST request sent to the EFA server. This can be used to use setting B<efa> does not yet cover, like C<--post lineRestriction=400> to also show IC and ICE trains. Note that B<--post> will be overridden by the standard efa options, such as B<--time>. =back