summaryrefslogtreecommitdiff
path: root/bin/hafas-m
diff options
context:
space:
mode:
Diffstat (limited to 'bin/hafas-m')
-rwxr-xr-xbin/hafas-m731
1 files changed, 632 insertions, 99 deletions
diff --git a/bin/hafas-m b/bin/hafas-m
index b2efd2b..ea8471f 100755
--- a/bin/hafas-m
+++ b/bin/hafas-m
@@ -1,23 +1,27 @@
-#!/usr/bin/env perl
+#!perl
use strict;
use warnings;
use 5.014;
-our $VERSION = '2.03';
+our $VERSION = '6.03';
+use utf8;
+use DateTime;
use Encode qw(decode);
-use Getopt::Long qw(:config no_ignore_case);
+use JSON;
+use Getopt::Long qw(:config no_ignore_case);
use List::MoreUtils qw(uniq);
-use List::Util qw(first max);
+use List::Util qw(first max);
use Travel::Status::DE::HAFAS;
-my ( $date, $time );
-my $arrivals = 0;
-my $ignore_late = 0;
-my $types = q{};
-my $language;
+my ( $date, $time, $language );
+my $arrivals;
+my $show_jid;
+my $types = q{};
my $developer_mode;
-my ( $list_services, $service, $hafas_url );
+my $via;
+my ( $json_output, $raw_json_output );
+my ( $list_services, $service );
my ( @excluded_mots, @exclusive_mots );
my @output;
@@ -27,45 +31,116 @@ for my $arg (@ARGV) {
$arg = decode( 'UTF-8', $arg );
}
+my $output_bold = -t STDOUT ? "\033[1m" : q{};
+my $output_reset = -t STDOUT ? "\033[0m" : q{};
+
GetOptions(
- 'a|arrivals' => \$arrivals,
- 'd|date=s' => \$date,
- 'h|help' => sub { show_help(0) },
- 'l|lang=s' => \$language,
- 'L|ignore-late' => \$ignore_late,
- 'm|mot=s' => \$types,
- 's|service=s' => \$service,
- 't|time=s' => \$time,
- 'u|url=s' => \$hafas_url,
- 'V|version' => \&show_version,
- 'devmode' => \$developer_mode,
- 'list' => \$list_services,
+ 'a|arrivals' => \$arrivals,
+ 'd|date=s' => \$date,
+ 'h|help' => sub { show_help(0) },
+ 'j|with-jid' => \$show_jid,
+ 'l|language=s' => \$language,
+ 'm|mot=s' => \$types,
+ 's|service=s' => \$service,
+ 't|time=s' => \$time,
+ 'v|via=s' => \$via,
+ 'V|version' => \&show_version,
+ 'devmode' => \$developer_mode,
+ 'json' => \$json_output,
+ 'raw-json' => \$raw_json_output,
+ 'list' => \$list_services,
) or show_help(1);
if ($list_services) {
- printf( "%-40s %-14s %s\n\n", 'operator', 'abbr. (-s)', 'url (-u)' );
+ printf(
+ "%-40s %-14s %-15s %s\n\n",
+ 'operator', 'abbr. (-s)', 'languages (-l)',
+ 'time zone'
+ );
for my $service ( Travel::Status::DE::HAFAS::get_services() ) {
- printf( "%-40s %-14s %s\n", @{$service}{qw(name shortname url)} );
+ printf(
+ "%-40s %-14s %-15s %s\n",
+ @{$service}{qw(name shortname)},
+ join( q{ }, @{ $service->{languages} // [] } ),
+ $service->{time_zone} // q{},
+ );
}
exit 0;
}
parse_mot_options();
-my $status = Travel::Status::DE::HAFAS->new(
- date => $date,
- language => $language,
+my %opt = (
excluded_mots => \@excluded_mots,
exclusive_mots => \@exclusive_mots,
station => shift || show_help(1),
- time => $time,
- mode => $arrivals ? 'arr' : 'dep',
+ arrivals => $arrivals,
developer_mode => $developer_mode,
service => $service,
- url => $hafas_url,
+ language => $language,
);
+if ( $opt{station} =~ m{ ^ (?<lat> [0-9.]+ ) : (?<lon> [0-9].+ ) $ }x ) {
+ $opt{geoSearch} = {
+ lat => $+{lat},
+ lon => $+{lon},
+ };
+ delete $opt{station};
+}
+elsif ( $opt{station} =~ m{ ^ [?] (?<query> .*) $ }x ) {
+ $opt{locationSearch} = $+{query};
+ delete $opt{station};
+}
+elsif ( $opt{station} =~ m{[|]} ) {
+ $opt{journey} = { id => $opt{station} };
+ delete $opt{station};
+}
+elsif ( $opt{station} =~ m{ ^ [!] (?<query> .*) $ }x ) {
+ $opt{journeyMatch} = $+{query};
+ delete $opt{station};
+}
+
+if ( $date or $time ) {
+ my $desc = Travel::Status::DE::HAFAS::get_service($service) // {};
+ my $dt
+ = DateTime->now( time_zone => $desc->{time_zone} // 'Europe/Berlin' );
+ if ($date) {
+ if ( $date
+ =~ m{ ^ (?<day> \d{1,2} ) [.] (?<month> \d{1,2} ) [.] (?<year> \d{4})? $ }x
+ )
+ {
+ $dt->set(
+ day => $+{day},
+ month => $+{month}
+ );
+ if ( $+{year} ) {
+ $dt->set( year => $+{year} );
+ }
+ }
+ else {
+ say "--date must be specified as DD.MM.[YYYY]";
+ exit 1;
+ }
+ }
+ if ($time) {
+ if ( $time =~ m{ ^ (?<hour> \d{1,2} ) : (?<minute> \d{1,2} ) $ }x ) {
+ $dt->set(
+ hour => $+{hour},
+ minute => $+{minute},
+ second => 0,
+ );
+ }
+ else {
+ say "--time must be specified as HH:MM";
+ exit 1;
+ }
+ }
+ $opt{datetime} = $dt;
+}
+
+my $status = Travel::Status::DE::HAFAS->new(%opt);
+
sub show_help {
my ($code) = @_;
@@ -82,27 +157,29 @@ sub show_version {
exit 0;
}
-sub parse_mot_options {
+sub spacer {
+ my ($len) = @_;
+ return ( $len % 2 ? q { } : q{} ) . ( q{ ·} x ( $len / 2 ) );
+}
+sub parse_mot_options {
my $default_yes = 1;
- if ( $types and $hafas_url ) {
- say STDERR 'The options -u and -m cannot be combined. Discarding -m';
- return;
- }
-
for my $type ( split( qr{,}, $types ) ) {
if ( $type eq 'help' or $type eq 'list' or $type eq q{?} ) {
- if ( not $hafas_url ) {
- $service //= 'DB';
- }
+ $service //= 'DB';
my $desc = Travel::Status::DE::HAFAS::get_service($service);
if ($desc) {
- my @mots = @{ $desc->{productbits} };
- @mots = grep { $_ ne 'x' } @mots;
- @mots = uniq @mots;
- @mots = sort @mots;
- say join( "\n", @mots );
+ for my $mot ( @{ $desc->{productbits} } ) {
+ if ( ref($mot) eq 'ARRAY' ) {
+ if ( $mot->[0] ne '_' ) {
+ printf( "%-10s %s\n", @{$mot} );
+ }
+ }
+ elsif ( $mot ne '_' ) {
+ say $mot;
+ }
+ }
exit 0;
}
else {
@@ -127,6 +204,37 @@ sub show_similar_stops {
for my $c (@candidates) {
printf( "%s (%s)\n", $c->{name}, $c->{id} );
}
+ return;
+ }
+ my $hafas = Travel::Status::DE::HAFAS->new(
+ locationSearch => $opt{station},
+ developer_mode => $developer_mode,
+ service => $service,
+ language => $language,
+ );
+ if ( $hafas->results ) {
+ say 'You might want to try one of the following stops:';
+ for my $r ( $hafas->results ) {
+ printf( "%s (%s)\n", $r->name, $r->eva );
+ }
+ }
+ return;
+}
+
+sub journey_has_via {
+ my ( $journey, $via ) = @_;
+
+ if ( $via =~ m{ ^ [0-9,]+ $ }x ) {
+ for my $eva ( split( qr{,}, $via ) ) {
+ if ( my $stop = first { $_->loc->eva == $eva } $journey->route ) {
+ return $stop;
+ }
+ }
+ return;
+ }
+
+ if ( my $stop = first { $_->loc->name =~ m{$via}io } $journey->route ) {
+ return $stop;
}
return;
}
@@ -146,11 +254,18 @@ sub display_result {
for my $line (@lines) {
- my $d = $line->[6];
- if ( $d->messages ) {
- print "\n";
- for my $msg ( $d->messages ) {
- printf( "# %s\n", $msg );
+ my $d = $line->[6];
+ my $first_message = 1;
+ for my $msg ( $d->messages ) {
+ if ( $msg->ref_count == 0 ) {
+ if ($first_message) {
+ print "\n";
+ $first_message = 0;
+ }
+ if ( $msg->short ) {
+ printf( "# %s\n", $msg->short );
+ }
+ printf( "# %s\n", $msg->text );
}
}
@@ -167,38 +282,388 @@ sub display_result {
return;
}
+sub display_occupancy {
+ my ($occupancy) = @_;
+
+ if ( not $occupancy ) {
+ return q{ };
+ }
+ if ( $occupancy == 1 ) {
+ return q{.};
+ }
+ if ( $occupancy == 2 ) {
+ return q{o};
+ }
+ if ( $occupancy == 3 ) {
+ return q{*};
+ }
+ if ( $occupancy == 4 ) {
+ return q{!};
+ }
+ return q{?};
+}
+
+sub format_delay {
+ my ( $delay, $len ) = @_;
+ if ( $delay and $len ) {
+ return sprintf( "(%+${len}d)", $delay );
+ }
+ return q{};
+}
+
if ( my $err = $status->errstr ) {
say STDERR "Request error: ${err}";
- if ( $status->errcode and $status->errcode eq 'H730' ) {
+ if ( $status->errcode
+ and ( $status->errcode eq 'H730' or $status->errcode eq 'LOCATION' )
+ and not $raw_json_output )
+ {
show_similar_stops();
}
exit 2;
}
-for my $d ( $status->results() ) {
-
- if ( $ignore_late and $d->delay ) {
- next;
- }
-
- push(
- @output,
- [
- $d->time,
- $d->is_cancelled
- ? 'CANCELED'
- : ( $d->delay ? sprintf( '%+d', $d->delay ) : q{} ),
- $d->train,
- $d->route_end,
- ( $d->platform // q{} ) . ( $d->is_changed_platform ? ' !' : q{} ),
- $d->info,
- $d
- ]
- );
+if ($raw_json_output) {
+ say JSON->new->convert_blessed->encode( $status->{raw_json} );
+ exit 0;
+}
+
+if ($json_output) {
+ if ( $opt{journey} ) {
+ say JSON->new->convert_blessed->encode( $status->result );
+ }
+ else {
+ say JSON->new->convert_blessed->encode( [ $status->results ] );
+ }
+ exit 0;
+}
+
+if ( $opt{journeyMatch} ) {
+ if ( scalar $status->results == 1 ) {
+ my ($journey) = $status->results;
+ $opt{journey} = { id => $journey->id };
+ delete $opt{journeyMatch};
+ $status = Travel::Status::DE::HAFAS->new(%opt);
+ if ( my $err = $status->errstr ) {
+ say STDERR "Request error: ${err}";
+ if (
+ $status->errcode
+ and
+ ( $status->errcode eq 'H730' or $status->errcode eq 'LOCATION' )
+ and not $raw_json_output
+ )
+ {
+ show_similar_stops();
+ }
+ exit 2;
+ }
+ }
+ else {
+ for my $result ( $status->results ) {
+ my $start = ( $result->route )[0];
+ my $end = ( $result->route )[-1];
+ say $result->id;
+ print $result->name;
+ if ( $result->number ) {
+ printf( " | Nr %s", $result->number );
+ }
+ if ( $result->line_no ) {
+ printf( " | Linie %s", $result->line_no );
+ }
+ say q{};
+ printf( "%s ab %s\n",
+ $start->dep->strftime('%H:%M'),
+ $start->loc->name );
+ printf( "%s an %s\n\n",
+ $end->arr->strftime('%H:%M'),
+ $end->loc->name );
+ }
+ exit 0;
+ }
+}
+
+if ( $opt{geoSearch} ) {
+ for my $result ( $status->results ) {
+ printf(
+ "%5.1f km %8d %s\n",
+ $result->distance_m * 1e-3,
+ $result->eva, $result->name
+ );
+ }
+ exit 0;
+}
+elsif ( $opt{locationSearch} ) {
+ for my $result ( $status->results ) {
+ printf( "%8d %s\n", $result->eva, $result->name );
+ }
+ exit 0;
+}
+elsif ( $opt{journey} ) {
+ my $result = $status->result;
+ my @prods;
+ my @directions;
+ my $prev_prod = 0;
+
+ printf( "%s → %s", $result->name, $result->route_end );
+ if ( $result->number ) {
+ printf( " / Nr %s", $result->number );
+ }
+ if ( $result->line_no ) {
+ printf( " / Linie %s", $result->line_no );
+ }
+ printf( "\nFahrt %s am %s\n",
+ $result->id, ( $result->route )[0]->sched_dep->strftime('%d.%m.%Y') );
+
+ my $delay_len = 0;
+ my $delay_fmt = 0;
+ my $occupancy_len = 0;
+ my $stop_len = 0;
+ for my $stop ( $result->route ) {
+ if ( $stop->delay ) {
+ $delay_len = max( $delay_len, length( $stop->delay ) + 1 );
+ }
+ if ( $stop->load and ( $stop->load->{FIRST} or $stop->load->{SECOND} ) )
+ {
+ $occupancy_len = 2;
+ }
+ if ( length( $stop->loc->name ) > $stop_len ) {
+ $stop_len = length( $stop->loc->name );
+ }
+ my $prod = $stop->prod_dep // $stop->prod_arr;
+ if ( $prod and $prod != $prev_prod ) {
+ push( @prods, $prod );
+ $prev_prod = $prod;
+ }
+ if ( $stop->direction ) {
+ push( @directions, $stop->direction );
+ }
+ }
+ if ($delay_len) {
+ $delay_fmt = $delay_len + 3;
+ }
+
+ if ( $result->operators ) {
+ printf( "Betrieb: %s\n", join( q{, }, $result->operators ) );
+ }
+
+ $prev_prod = 0;
+
+ my $desc = Travel::Status::DE::HAFAS::get_service($service) // {};
+ my $now
+ = DateTime->now( time_zone => $desc->{time_zone} // 'Europe/Berlin' );
+ my $mark_stop = 0;
+ for my $i ( reverse 1 .. scalar $result->route ) {
+ my $stop = ( $result->route )[ $i - 1 ];
+ if ( not $stop->dep_cancelled and $stop->dep and $now <= $stop->dep ) {
+ $mark_stop = $stop;
+ }
+ elsif ( not $stop->arr_cancelled and $stop->arr and $now <= $stop->arr )
+ {
+ $mark_stop = $stop;
+ }
+ }
+
+ my $message_id = 1;
+
+ print "\n";
+ for my $stop ( $result->route ) {
+ my $msg_line = q{};
+ for my $message ( $stop->messages ) {
+ if ( $message->ref_count > 0
+ and $message->code ne
+ 'text.journeystop.product.or.direction.changes.stop.message'
+ and $message->text ne 'Halt entfällt' )
+ {
+ if ( not $message->{id} ) {
+ $message->{id} = $message_id++;
+ }
+ $msg_line .= sprintf( ' (%d)', $message->{id} );
+ }
+ }
+
+ my $prod_line = q{};
+ if ( @prods > 1 ) {
+ my $prod = $stop->prod_dep // $stop->prod_arr;
+ if ( $prod and $prod != $prev_prod ) {
+ $prod_line
+ = sprintf( " %s (%s)", $prod->name, $prod->operator );
+ $prev_prod = $prod;
+ }
+ }
+
+ my $dir_line = q{};
+ if ( @directions > 1 and $stop->direction ) {
+ $dir_line = ' → ' . $stop->direction;
+ }
+
+ my $tz_line = q{};
+ if ( $stop->tz_offset and ( $stop->arr or $stop->dep ) ) {
+ $tz_line = ( $prod_line or $dir_line ) ? q{ · } : q{ };
+ $tz_line .= 'local ';
+ if ( $stop->arr ) {
+ $tz_line
+ .= $stop->arr->clone->add( minutes => $stop->tz_offset )
+ ->strftime('%H:%M');
+ }
+ if ( $stop->arr and $stop->dep ) {
+ $tz_line .= ' → ';
+ }
+ if ( $stop->dep ) {
+ $tz_line
+ .= $stop->dep->clone->add( minutes => $stop->tz_offset )
+ ->strftime('%H:%M');
+ }
+ $tz_line .= q{ };
+ }
+
+ printf(
+"%s%5s %s %5s %-${delay_fmt}s%${occupancy_len}s%-${occupancy_len}s %s%s%s%s%s%s%s\n",
+ $stop == $mark_stop ? $output_bold : q{},
+ $stop->arr_cancelled ? '--:--'
+ : ( $stop->arr ? $stop->arr->strftime('%H:%M') : q{} ),
+ ( $stop->arr and $stop->dep ) ? '→' : q{ },
+ $stop->dep_cancelled ? '--:--'
+ : ( $stop->dep ? $stop->dep->strftime('%H:%M') : q{} ),
+ format_delay( $stop->delay, $delay_len ),
+ $stop->load->{FIRST} ? display_occupancy( $stop->load->{FIRST} )
+ : q{},
+ $stop->load->{SECOND} ? display_occupancy( $stop->load->{SECOND} )
+ : q{},
+ $stop->loc->name,
+ $stop == $mark_stop ? $output_reset : q{},
+ ( $tz_line or $prod_line or $dir_line or $msg_line )
+ ? spacer( $stop_len + 1 - length( $stop->loc->name ) )
+ : q{},
+ $prod_line,
+ $dir_line,
+ $tz_line,
+ $msg_line,
+ );
+ }
+
+ for my $msg ( $result->messages ) {
+ if ( $msg->code eq
+ 'text.journeystop.product.or.direction.changes.journey.message' )
+ {
+ next;
+ }
+ say '';
+ if ( $msg->short ) {
+ printf( "%s\n", $msg->short );
+ }
+ printf( "%s\n", $msg->text );
+ }
+
+ for my $msg ( $status->messages ) {
+ if ( $msg->{id} ) {
+ say '';
+ if ( $msg->short ) {
+ printf( "(%d) %s\n", $msg->{id}, $msg->short );
+ }
+ printf( "(%d) %s\n", $msg->{id}, $msg->text );
+ }
+ }
+ exit 0;
+}
+
+my @results = map { $_->[1] }
+ sort { $a->[0] <=> $b->[0] }
+ map { [ $_->datetime->epoch, $_ ] } $status->results;
+
+if ($via) {
+ @results = grep { journey_has_via( $_, $via ) } @results;
+}
+
+my $delay_len = 0;
+my $occupancy_len = 0;
+my $offset_len = 0;
+for my $d (@results) {
+ if ( $d->delay ) {
+ $delay_len = max( $delay_len, length( $d->delay ) + 1 );
+ }
+ if ( $d->load and ( $d->load->{FIRST} or $d->load->{SECOND} ) ) {
+ $occupancy_len = 2;
+ }
+ if ( $d->tz_offset ) {
+ $offset_len = 1;
+ }
+}
+
+my $message_id = 1;
+for my $m ( $status->messages ) {
+ if ( $m->ref_count > 0 ) {
+ $m->{id} = $message_id++;
+ }
+}
+
+for my $d (@results) {
+
+ my $info_line = q{};
+
+ for my $message ( $d->messages ) {
+ if ( $message->ref_count > 0 ) {
+ $message->{show} = 1;
+ $info_line = sprintf( '(%d) %s', $message->{id}, $info_line );
+ }
+ }
+
+ if ( $d->load ) {
+ $info_line
+ = display_occupancy( $d->load->{FIRST} )
+ . display_occupancy( $d->load->{SECOND} ) . ' '
+ . $info_line;
+ }
+
+ if ($show_jid) {
+ $info_line = $d->id . ' ' . $info_line;
+ }
+
+ my $entry = [
+ ( $d->is_cancelled ? '--:--' : $d->datetime->strftime('%H:%M') )
+ . ( $d->tz_offset ? q{*} : ( q{ } x $offset_len ) ),
+ $d->is_cancelled
+ ? q{}
+ : format_delay( $d->delay, $delay_len ),
+ $d->name,
+ $d->route_end,
+ ( $d->platform // q{} ) . ( $d->is_changed_platform ? ' !' : q{} ),
+ $info_line,
+ $d
+ ];
+
+ if ($via) {
+ my $stop = journey_has_via( $d, $via );
+
+ # HAFAS does not provide real-time data for route entries, so we have to guesstimate the arrival time
+ $entry->[0] .= ' → '
+ . (
+ $stop->arr_cancelled
+ ? '--:--'
+ : $stop->arr->clone->add( minutes => $d->delay // 0 )
+ ->strftime('%H:%M')
+ );
+ }
+
+ push( @output, $entry, );
}
display_result(@output);
+if ($offset_len) {
+ printf( "\n* reported for %s; local time differs\n",
+ $status->get_active_service->{time_zone} // 'Europe/Berlin' );
+}
+
+for my $m ( $status->messages ) {
+ if ( $m->ref_count > 0 and $m->{show} ) {
+ if ( $m->short ) {
+ printf( "\n# (%d) %s\n# %s\n", $m->{id}, $m->short, $m->text );
+ }
+ else {
+ printf( "\n# (%d) %s\n", $m->{id}, $m->text );
+ }
+ }
+}
+
__END__
=head1 NAME
@@ -207,21 +672,70 @@ hafas-m - Interface to the DeutscheBahn/HAFAS online departure monitor
=head1 SYNOPSIS
-B<hafas-m> [B<-d> I<date>] [B<-t> I<time>] [B<-m> I<motlist>]
-[B<-s> I<service> | B<-u> I<url>] I<station>
+B<hafas-m> [B<-a>] [B<-d> I<date>] [B<-t> I<time>] [B<-m> I<motlist>]
+[B<-v> I<via>] [B<-s> I<service>] [B<-l> I<language>] I<station>
+
+B<hafas-m> [B<-s> I<service>] B<?>I<query>|I<lat>B<:>I<lon>
+
+B<hafas-m> [B<-s> I<service>] [B<-l> I<language>] B<!>I<query>|I<journeyID>
=head1 VERSION
-version 2.03
+version 6.03
=head1 DESCRIPTION
-hafas-m is an interface to HAFAS-based departure monitors, for instance the
-one available at L<http://reiseauskunft.bahn.de/bin/bhftafel.exe/dn>.
+hafas-m is an interface to HAFAS public transport services such as the one
+operated by Deutsche Bahn.
+
+It has four operating modes that depend on the contents of its argument.
+
+=head2 Arrival/Departure Monitor (I<station>)
+
+Show departures (or arrivals) at I<station>, optionally filtered by mode of
+transport. I<station> may be given as a station name or HAFAS station ID.
+For each train, B<hafas-m> shows
+
+=over
+
+=item * estimated departure (or arrival) time,
+
+=item * delay, if known,
+
+=item * trip name, number, or line,
-It requests all departures at I<station> (optionally filtered by date, time,
-route and means of transport) and lists them on stdout, similar to the big
-departure screens installed at most main stations.
+=item * direction / destination,
+
+=item * platform, if known (B<!> indicates a platform change), and
+
+=item * expected occupancy of first and second class, if known.
+
+=back
+
+Times are always given in the selected service's default time zone (see
+B<--list>). Times that differ from the queried stop's local time are marked
+with C<< * >>. Occupancy indicators are, from least occupied to fully booked:
+B<.> B<o> B<*> B<!>.
+
+=head2 Location Search (B<?>I<query>|I<lat>B<:>I<lon>)
+
+List stations that match I<query> or that are located in the vicinity of
+I<lat>B<:>I<lon> geocoordinates with EVA ID and name.
+
+=head2 Trip Search (B<!>I<query>)
+
+Show trip details (see below) for the train number provided in I<query>
+(e.g. "ICE 205" or "S 31111") if it resolves into a single journey ID.
+Otherwise, list all journey IDs that match I<query>.
+
+=head2 Trip Details (I<journeyID>)
+
+List intermediate stops of I<journeyID> with arrival/departure time, delay (if
+available), occupancy (if available), and stop name. Also includes
+line/journey, operator, and heading information.
+
+Times are reported in the selected HAFAS service's default time zone (typically
+Europe/Berlin). If a stop's local time differs, it is also provided.
=head1 OPTIONS
@@ -233,22 +747,31 @@ Show arrivals instead of departures, including trains ending at the specified
station. Note that this causes the output to display the start instead of
the end station.
-=item B<-d>, B<--date> I<dd>.I<mm>.I<yyyy>
+=item B<-d>, B<--date> I<dd>.I<mm>.[I<yyyy>]
Date to list departures for. Default: today.
-=item B<-l>, B<--lang> B<d>|B<e>|B<i>|B<n>
+=item B<-j>, B<--with-jid>
-Set language used for additional information. Supports B<d>eutsch (default),
-B<e>nglish, B<i>talian and dutch (B<n>), depending on the used service.
+Show journey IDs in arrival/departure board. These can be used to obtain
+details on individual journeys with subsequent B<hafas-m> invocations.
-=item B<-L>, B<--ignore-late>
+=item B<--json>
-Do not display delayed trains.
+Print result(s) as JSON and exit. This is a dump of internal data structures
+and not guaranteed to remain stable between minor versions. Please use the
+Travel::Status::DE::HAFAS(3pm) module if you need a proper API.
+
+=item B<-l>, B<--language> I<language>
+
+Request free-text messages to be provided in I<language>.
+See B<--list> for a list of languages supported by individual HAFAS instances.
+Note that requesting an invalid/unsupported language may lead to garbage output.
=item B<--list>
-List known HAFAS installations. See also B<--service> and B<--url>.
+List known HAFAS instances and exit. Use B<-s>|B<--service> to select a
+service from this list for a HAFAS request.
=item B<-m>, B<--mot> I<motlist>
@@ -262,6 +785,11 @@ To show them exclusively, set I<motlist> to I<mot1>,I<mot2>,...
The I<mot> types depend on the used service. Use C<< -m help >> to list them.
+=item B<--raw-json>
+
+Print unprocessed HAFAS response as JSON and exit.
+Useful for debugging and development purposes.
+
=item B<-s>, B<--service> I<service>
Request arrivals/departures using the API provided by I<service>, defaults
@@ -269,26 +797,25 @@ to DB (Deutsche Bahn). See B<--list> for a list of known services.
=item B<-t>, B<--time> I<hh>:I<mm>
-Time to list departures for. Default: now.
-
-=item B<-u>, B<--url> I<url>
+Time to list departures for. Must be specified in the selected HAFAS
+service's default time zone, see B<--list>. Default: now.
-Request arrivals/departures using the API entry point at I<url>, defaults to
-C<< http://reiseauskunft.bahn.de/bin/bhftafel.exe >>. Note that the language
-and output selection suffix (e.g. "/dn") must not be included here.
+=item B<-v>, B<--via> I<stopname>|I<eva1>,I<eva2>,...
-Again, see B<--list> for a list of known URLs. Unknown URLs are also
-supported, though note that B<--mot> will not work when using this opton.
+Only show departures that pass by I<stopname> (or arivals that have passed by
+I<stopname>). If I<stopname> is given as a list of numeric EVA IDs, only
+arrivals/departures with an exact EVA ID match are shown. Otherwise I<stopname>
+is treated as a regular expression and matched against stop names.
=item B<-V>, B<--version>
-Show version information.
+Show version information and exit.
=back
=head1 EXIT STATUS
-Zero unless things went wrong.
+0 upon success, 1 upon internal error, 2 upon backend error.
=head1 CONFIGURATION
@@ -302,17 +829,23 @@ None.
=item * LWP::UserAgent(3pm)
-=item * XML::LibXML(3pm)
-
=back
=head1 BUGS AND LIMITATIONS
-The non-default services (anything other than DB) are not well tested.
+=over
+
+=item * The non-default services (anything other than DB) are not well-tested.
+
+=item * HAFAS does not provide real-time data for routes of stationboard
+entries. Hence, B<--via> estimates the arrival time from scheduled
+departure and departure delay
+
+=back
=head1 AUTHOR
-Copyright (C) 2015-2017 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt>
+Copyright (C) 2015-2023 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
=head1 LICENSE