summaryrefslogtreecommitdiff
path: root/lib/Travel/Status/DE/IRIS.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travel/Status/DE/IRIS.pm')
-rw-r--r--lib/Travel/Status/DE/IRIS.pm607
1 files changed, 527 insertions, 80 deletions
diff --git a/lib/Travel/Status/DE/IRIS.pm b/lib/Travel/Status/DE/IRIS.pm
index c4bbeda..60f5d49 100644
--- a/lib/Travel/Status/DE/IRIS.pm
+++ b/lib/Travel/Status/DE/IRIS.pm
@@ -4,16 +4,14 @@ use strict;
use warnings;
use 5.014;
-no if $] >= 5.018, warnings => 'experimental::smartmatch';
-
-our $VERSION = '1.28';
+our $VERSION = '1.96';
use Carp qw(confess cluck);
use DateTime;
use DateTime::Format::Strptime;
-use List::Util qw(first);
+use List::Util qw(none first);
use List::MoreUtils qw(uniq);
-use List::UtilsBy qw(uniq_by);
+use List::UtilsBy qw(uniq_by);
use LWP::UserAgent;
use Travel::Status::DE::IRIS::Result;
use XML::LibXML;
@@ -31,6 +29,153 @@ sub try_load_xml {
return ( $tree, undef );
}
+# "station" parameter must be an EVA or DS100 ID.
+sub new_p {
+ my ( $class, %opt ) = @_;
+ my $promise = $opt{promise}->new;
+
+ if ( not $opt{station} ) {
+ return $promise->reject('station flag must be passed');
+ }
+
+ my $self = $class->new( %opt, async => 1 );
+ $self->{promise} = $opt{promise};
+
+ my $lookahead_steps = int( $self->{lookahead} / 60 );
+ if ( ( 60 - $self->{datetime}->minute ) < ( $self->{lookahead} % 60 ) ) {
+ $lookahead_steps++;
+ }
+ my $lookbehind_steps = int( $self->{lookbehind} / 60 );
+ if ( $self->{datetime}->minute < ( $self->{lookbehind} % 60 ) ) {
+ $lookbehind_steps++;
+ }
+
+ my @candidates = $opt{get_station}( $opt{station} );
+
+ if ( @candidates != 1 and $opt{station} =~ m{^\d+$} ) {
+ @candidates = (
+ [
+ "D$opt{station}", "Betriebsstelle nicht bekannt $opt{station}",
+ $opt{station}
+ ]
+ );
+ }
+
+ if ( @candidates == 0 ) {
+ return $promise->reject('station not found');
+ }
+ if ( @candidates >= 2 ) {
+ return $promise->reject('station identifier is ambiguous');
+ }
+
+ # "uic" is deprecated
+ $self->{station} = {
+ ds100 => $candidates[0][0],
+ eva => $candidates[0][2],
+ name => $candidates[0][1],
+ uic => $candidates[0][2],
+ };
+ $self->{related_stations} = [];
+
+ my @queue = ( $self->{station}{eva} );
+ my @related_reqs;
+ my @related_stations;
+ my %seen = ( $self->{station}{eva} => 1 );
+ my $iter_depth = 0;
+
+ while ( @queue and $iter_depth < 12 and $opt{with_related} ) {
+ my $eva = shift(@queue);
+ $iter_depth++;
+ for my $ref ( @{ $opt{meta}{$eva} // [] } ) {
+ if ( not $seen{$ref} ) {
+ push( @related_stations, $ref );
+ $seen{$ref} = 1;
+ push( @queue, $ref );
+ }
+ }
+ }
+
+ for my $eva (@related_stations) {
+ @candidates = $opt{get_station}($eva);
+
+ if ( @candidates == 1 ) {
+
+ # "uic" is deprecated
+ push(
+ @{ $self->{related_stations} },
+ {
+ ds100 => $candidates[0][0],
+ eva => $candidates[0][2],
+ name => $candidates[0][1],
+ uic => $candidates[0][2],
+ }
+ );
+ }
+ }
+
+ my $dt_req = $self->{datetime}->clone;
+ my @timetable_reqs
+ = ( $self->get_timetable_p( $self->{station}{eva}, $dt_req ) );
+
+ for my $eva (@related_stations) {
+ push( @timetable_reqs, $self->get_timetable_p( $eva, $dt_req ) );
+ }
+
+ for ( 1 .. $lookahead_steps ) {
+ $dt_req->add( hours => 1 );
+ push( @timetable_reqs,
+ $self->get_timetable_p( $self->{station}{eva}, $dt_req ) );
+ for my $eva (@related_stations) {
+ push( @timetable_reqs, $self->get_timetable_p( $eva, $dt_req ) );
+ }
+ }
+
+ $dt_req = $self->{datetime}->clone;
+ for ( 1 .. $lookbehind_steps ) {
+ $dt_req->subtract( hours => 1 );
+ push( @timetable_reqs,
+ $self->get_timetable_p( $self->{station}{eva}, $dt_req ) );
+ for my $eva (@related_stations) {
+ push( @timetable_reqs, $self->get_timetable_p( $eva, $dt_req ) );
+ }
+ }
+
+ $self->{promise}->all(@timetable_reqs)->then(
+ sub {
+ my @realtime_reqs
+ = ( $self->get_realtime_p( $self->{station}{eva} ) );
+ for my $eva (@related_stations) {
+ push( @realtime_reqs, $self->get_realtime_p( $eva, $dt_req ) );
+ }
+ return $self->{promise}->all_settled(@realtime_reqs);
+ }
+ )->then(
+ sub {
+ my @realtime_results = @_;
+
+ for my $realtime_result (@realtime_results) {
+ if ( $realtime_result->{status} eq 'rejected' ) {
+ $self->{warnstr} //= q{};
+ $self->{warnstr}
+ .= "Realtime data request failed: $realtime_result->{reason}. ";
+ }
+ }
+
+ $self->postprocess_results;
+ $promise->resolve($self);
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
sub new {
my ( $class, %opt ) = @_;
@@ -43,11 +188,12 @@ sub new {
// DateTime->now( time_zone => 'Europe/Berlin' ),
developer_mode => $opt{developer_mode},
iris_base => $opt{iris_base}
- // 'http://iris.noncd.db.de/iris-tts/timetable',
- lookahead => $opt{lookahead} // ( 2 * 60 ),
- lookbehind => $opt{lookbehind} // ( 0 * 60 ),
- main_cache => $opt{main_cache},
- rt_cache => $opt{realtime_cache},
+ // 'https://iris.noncd.db.de/iris-tts/timetable',
+ keep_transfers => $opt{keep_transfers},
+ lookahead => $opt{lookahead} // ( 2 * 60 ),
+ lookbehind => $opt{lookbehind} // ( 0 * 60 ),
+ main_cache => $opt{main_cache},
+ rt_cache => $opt{realtime_cache},
serializable => $opt{serializable},
user_agent => $opt{user_agent},
with_related => $opt{with_related},
@@ -64,6 +210,19 @@ sub new {
bless( $self, $class );
+ my $lookahead_steps = int( $self->{lookahead} / 60 );
+ if ( ( 60 - $self->{datetime}->minute ) < ( $self->{lookahead} % 60 ) ) {
+ $lookahead_steps++;
+ }
+ my $lookbehind_steps = int( $self->{lookbehind} / 60 );
+ if ( $self->{datetime}->minute < ( $self->{lookbehind} % 60 ) ) {
+ $lookbehind_steps++;
+ }
+
+ if ( $opt{async} ) {
+ return $self;
+ }
+
if ( not $self->{user_agent} ) {
my %lwp_options = %{ $opt{lwp_options} // { timeout => 10 } };
$self->{user_agent} = LWP::UserAgent->new(%lwp_options);
@@ -80,16 +239,24 @@ sub new {
$self->{related_stations} = \@related_stations;
for my $ref (@related_stations) {
+
+ # We (the parent) perform transfer processing, so child requests must not
+ # do it themselves. Otherwise, trains from child requests will be
+ # processed twice and may be lost.
+ # Similarly, child requests must not perform requests to related
+ # stations -- we're already doing that right now.
my $ref_status = Travel::Status::DE::IRIS->new(
datetime => $self->{datetime},
developer_mode => $self->{developer_mode},
+ iris_base => $self->{iris_base},
lookahead => $self->{lookahead},
lookbehind => $self->{lookbehind},
- station => $ref->{uic},
+ station => $ref->{eva},
main_cache => $self->{main_cache},
realtime_cache => $self->{rt_cache},
strptime_obj => $self->{strptime_obj},
user_agent => $self->{user_agent},
+ keep_transfers => 1,
with_related => 0,
);
if ( not $ref_status->errstr ) {
@@ -101,50 +268,52 @@ sub new {
return $self;
}
- my $lookahead_steps = int( $self->{lookahead} / 60 );
- if ( ( 60 - $self->{datetime}->minute ) < ( $self->{lookahead} % 60 ) ) {
- $lookahead_steps++;
- }
- my $lookbehind_steps = int( $self->{lookbehind} / 60 );
- if ( $self->{datetime}->minute < ( $self->{lookbehind} % 60 ) ) {
- $lookbehind_steps++;
- }
-
my $dt_req = $self->{datetime}->clone;
- $self->get_timetable( $self->{station}{uic}, $dt_req );
+ $self->get_timetable( $self->{station}{eva}, $dt_req );
for ( 1 .. $lookahead_steps ) {
$dt_req->add( hours => 1 );
- $self->get_timetable( $self->{station}{uic}, $dt_req );
+ $self->get_timetable( $self->{station}{eva}, $dt_req );
}
$dt_req = $self->{datetime}->clone;
for ( 1 .. $lookbehind_steps ) {
$dt_req->subtract( hours => 1 );
- $self->get_timetable( $self->{station}{uic}, $dt_req );
+ $self->get_timetable( $self->{station}{eva}, $dt_req );
}
$self->get_realtime;
- # tra (transfer?) indicates a train changing its ID, so there are two
- # results for the same train. Remove the departure-only trains from the
- # result set and merge them with their arrival-only counterpart.
- # This way, in case the arrival is available but the departure isn't,
- # nothing gets lost.
- my @merge_candidates
- = grep { $_->transfer and $_->departure } @{ $self->{results} };
- @{ $self->{results} }
- = grep { not( $_->transfer and $_->departure ) } @{ $self->{results} };
+ $self->postprocess_results;
+
+ return $self;
+}
- for my $transfer (@merge_candidates) {
- my $result
- = first { $_->transfer and $_->transfer eq $transfer->train_id }
- @{ $self->{results} };
- if ($result) {
- $result->merge_with_departure($transfer);
+sub postprocess_results {
+ my ($self) = @_;
+ if ( not $self->{keep_transfers} ) {
+
+ # tra (transfer?) indicates a train changing its ID, so there are two
+ # results for the same train. Remove the departure-only trains from the
+ # result set and merge them with their arrival-only counterpart.
+ # This way, in case the arrival is available but the departure isn't,
+ # nothing gets lost.
+ my @merge_candidates
+ = grep { $_->transfer and $_->departure } @{ $self->{results} };
+ @{ $self->{results} }
+ = grep { not( $_->transfer and $_->departure ) }
+ @{ $self->{results} };
+
+ for my $transfer (@merge_candidates) {
+ my $result
+ = first { $_->transfer and $_->transfer eq $transfer->train_id }
+ @{ $self->{results} };
+ if ($result) {
+ $result->merge_with_departure($transfer);
+ }
}
}
@{ $self->{results} } = grep {
- my $d = $_->departure // $_->arrival;
+ my $d = $_->departure // $_->arrival;
my $s = $_->sched_arrival // $_->sched_departure // $_->arrival // $d;
$d = $d->subtract_datetime( $self->{datetime} );
$s = $s->subtract_datetime( $self->{datetime} );
@@ -160,8 +329,55 @@ sub new {
# same goes for replacement refs (the <ref> tag in the fchg document)
$self->create_replacement_refs;
+}
- return $self;
+sub get_with_cache_p {
+ my ( $self, $cache, $url ) = @_;
+
+ if ( $self->{developer_mode} ) {
+ say "GET $url";
+ }
+
+ my $promise = $self->{promise}->new;
+
+ if ($cache) {
+ my $content = $cache->thaw($url);
+ if ($content) {
+ if ( $self->{developer_mode} ) {
+ say ' cache hit';
+ }
+ return $promise->resolve($content);
+ }
+ }
+
+ if ( $self->{developer_mode} ) {
+ say ' cache miss';
+ }
+
+ my $res = $self->{user_agent}->get_p($url)->then(
+ sub {
+ my ($tx) = @_;
+ if ( my $err = $tx->error ) {
+ $promise->reject(
+ "GET $url returned HTTP $err->{code} $err->{message}");
+ return;
+ }
+ my $content = $tx->res->body;
+ if ($cache) {
+ $cache->freeze( $url, \$content );
+ }
+ $promise->resolve($content);
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
}
sub get_with_cache {
@@ -200,17 +416,67 @@ sub get_with_cache {
return ( $content, undef );
}
+sub get_station_p {
+ my ( $self, %opt ) = @_;
+
+ my $promise = $self->{promise}->new;
+ my $station = $opt{name};
+
+ $self->get_with_cache_p( $self->{main_cache},
+ $self->{iris_base} . '/station/' . $station )->then(
+ sub {
+ my ($raw) = @_;
+ my ( $xml_st, $xml_err ) = try_load_xml($raw);
+ if ($xml_err) {
+ $promise->reject('Failed to parse station data: Invalid XML');
+ return;
+ }
+ my $station_node = ( $xml_st->findnodes('//station') )[0];
+
+ if ( not $station_node ) {
+ $promise->reject(
+ "Station '$station' has no associated timetable");
+ return;
+ }
+ $promise->resolve(
+ {
+ ds100 => $station_node->getAttribute('ds100'),
+ eva => $station_node->getAttribute('eva'),
+ name => $station_node->getAttribute('name'),
+ uic => $station_node->getAttribute('eva'),
+ }
+ );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
sub get_station {
my ( $self, %opt ) = @_;
my $iter_depth = 0;
my @ret;
my @queue = ( $opt{name} );
- my @seen;
+
+ # @seen holds station IDs which were already seen during recursive
+ # 'meta' descent. This avoids infinite loops of 'meta' references.
+ # Additionally, we use it to skip stations shat should not be referenced.
+ # This includes Norddeich / Norddeich Mole (different stations commonly used
+ # by identical trains with different departure times), and Essen-Dellwig /
+ # Essen-Dellwig Ost (different stations used by different trains, but with
+ # identical platform numbers).
+ my @seen = ( 8007768, 8004449, 8001903, 8001904 );
while ( @queue and $iter_depth < 12 ) {
my $station = shift(@queue);
- push( @seen, $station );
$iter_depth++;
my ( $raw, $err )
@@ -237,6 +503,9 @@ sub get_station {
my $station_node = ( $xml_st->findnodes('//station') )[0];
if ( not $station_node ) {
+ if ( $self->{developer_mode} ) {
+ say ' no timetable';
+ }
if ( $opt{root} ) {
$self->{errstr}
= "Station '$station' has no associated timetable";
@@ -259,24 +528,42 @@ sub get_station {
next;
}
+ if ( $station_node->getAttribute('ds100') =~ m{ ^ D \d+ $ }x ) {
+
+ # This used to indicate an invalid DS100 code, at least from DB
+ # perspective. It typically referred to subway stations which do not
+ # have IRIS departures.
+ # However, since Fahrplanwechsel 2022 / 2023, this does not seem
+ # to be the case anymore. There are some stations whose DS100 code
+ # IRIS does not know, for whatever reason. So for now, accept these
+ # stations as well.
+
+ #next;
+ }
+
push(
@ret,
{
- uic => $station_node->getAttribute('eva'),
- name => $station_node->getAttribute('name'),
ds100 => $station_node->getAttribute('ds100'),
+ eva => $station_node->getAttribute('eva'),
+ name => $station_node->getAttribute('name'),
+ uic => $station_node->getAttribute('eva'),
}
);
if ( $self->{developer_mode} ) {
- printf( " -> %s (%s / %s)\n", @{ $ret[-1] }{qw{name uic ds100}} );
+ printf( " -> %s (%s / %s)\n", @{ $ret[-1] }{qw{name eva ds100}} );
}
if ( $opt{recursive} and defined $station_node->getAttribute('meta') ) {
my @refs
= uniq( split( m{ \| }x, $station_node->getAttribute('meta') ) );
- @refs = grep { not( $_ ~~ \@seen or $_ ~~ \@queue ) } @refs;
- push( @queue, @refs );
+ for my $ref (@refs) {
+ if ( none { $_ == $ref } @seen and none { $_ == $ref } @queue )
+ {
+ push( @queue, $ref );
+ }
+ }
$opt{root} = 0;
}
}
@@ -286,13 +573,13 @@ sub get_station {
. "This is probably a bug" );
}
- @ret = uniq_by { $_->{uic} } @ret;
+ @ret = uniq_by { $_->{eva} } @ret;
return @ret;
}
sub add_result {
- my ( $self, $station, $s ) = @_;
+ my ( $self, $station_name, $station_eva, $s ) = @_;
my $id = $s->getAttribute('id');
my $e_tl = ( $s->findnodes( $self->{xp_tl} ) )[0];
@@ -305,13 +592,15 @@ sub add_result {
my %data = (
raw_id => $id,
- classes => $e_tl->getAttribute('f'), # D N S F
- train_no => $e_tl->getAttribute('n'), # dep number
- type => $e_tl->getAttribute('c'), # S/ICE/ERB/...
- station => $station,
+ classes => $e_tl->getAttribute('f'), # D N S F
+ operator => $e_tl->getAttribute('o'), # coded operator: 03/80/R2/...
+ train_no => $e_tl->getAttribute('n'), # dep number
+ type => $e_tl->getAttribute('c'), # S/ICE/ERB/...
+ station => $station_name,
+ station_eva => $station_eva + 0, # EVA IDs are numbers
+ station_uic => $station_eva + 0, # deprecated
strptime_obj => $self->{strptime_obj},
- #unknown_o => $e_tl->getAttribute('o'), # owner: 03/80/R2/...
#unknown_t => $e_tl->getAttribute('t'), # p
);
@@ -322,9 +611,8 @@ sub add_result {
$data{route_pre} = $e_ar->getAttribute('ppth');
$data{route_start} = $e_ar->getAttribute('pde');
$data{transfer} = $e_ar->getAttribute('tra');
+ $data{arrival_hidden} = $e_ar->getAttribute('hi');
$data{arrival_wing_ids} = $e_ar->getAttribute('wings');
-
- #$data{unk_ar_hi} = $e_ar->getAttribute('hi');
}
if ($e_dp) {
@@ -334,9 +622,8 @@ sub add_result {
$data{route_post} = $e_dp->getAttribute('ppth');
$data{route_end} = $e_dp->getAttribute('pde');
$data{transfer} = $e_dp->getAttribute('tra');
+ $data{departure_hidden} = $e_dp->getAttribute('hi');
$data{departure_wing_ids} = $e_dp->getAttribute('wings');
-
- #$data{unk_dp_hi} = $e_dp->getAttribute('hi');
}
if ( $data{arrival_wing_ids} ) {
@@ -360,6 +647,41 @@ sub add_result {
return $result;
}
+sub get_timetable_p {
+ my ( $self, $eva, $dt ) = @_;
+
+ my $promise = $self->{promise}->new;
+
+ $self->get_with_cache_p( $self->{main_cache},
+ $dt->strftime( $self->{iris_base} . "/plan/${eva}/%y%m%d/%H" ) )->then(
+ sub {
+ my ($raw) = @_;
+ my ( $xml, $xml_err ) = try_load_xml($raw);
+ if ($xml_err) {
+ $promise->reject(
+ 'Failed to parse a schedule part: Invalid XML');
+ return;
+ }
+ my $station
+ = ( $xml->findnodes('/timetable') )[0]->getAttribute('station');
+
+ for my $s ( $xml->findnodes('/timetable/s') ) {
+
+ $self->add_result( $station, $eva, $s );
+ }
+ $promise->resolve;
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+ return $promise;
+}
+
sub get_timetable {
my ( $self, $eva, $dt ) = @_;
@@ -383,16 +705,51 @@ sub get_timetable {
for my $s ( $xml->findnodes('/timetable/s') ) {
- $self->add_result( $station, $s );
+ $self->add_result( $station, $eva, $s );
+ }
+
+ if ( $self->{developer_mode}
+ and not scalar $xml->findnodes('/timetable/s') )
+ {
+ say ' no scheduled trains';
}
return $self;
}
+sub get_realtime_p {
+ my ( $self, $eva ) = @_;
+
+ my $promise = $self->{promise}->new;
+
+ $self->get_with_cache_p( $self->{rt_cache},
+ $self->{iris_base} . "/fchg/${eva}" )->then(
+ sub {
+ my ($raw) = @_;
+ my ( $xml, $xml_err ) = try_load_xml($raw);
+ if ($xml_err) {
+ $promise->reject(
+ 'Failed to parse a schedule part: Invalid XML');
+ return;
+ }
+ $self->parse_realtime( $eva, $xml );
+ $promise->resolve;
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject("Failed to fetch realtime data: $err");
+ return;
+ }
+ )->wait;
+ return $promise;
+}
+
sub get_realtime {
my ($self) = @_;
- my $eva = $self->{station}{uic};
+ my $eva = $self->{station}{eva};
my ( $raw, $err )
= $self->get_with_cache( $self->{rt_cache},
@@ -410,6 +767,11 @@ sub get_realtime {
return $self;
}
+ $self->parse_realtime( $eva, $xml );
+}
+
+sub parse_realtime {
+ my ( $self, $eva, $xml ) = @_;
my $station = ( $xml->findnodes('/timetable') )[0]->getAttribute('station');
for my $s ( $xml->findnodes('/timetable/s') ) {
@@ -426,7 +788,7 @@ sub get_realtime {
# add_result will return nothing if no ./tl node is present. The ./tl
# check here is for optimization purposes.
if ( not $result and ( $s->findnodes( $self->{xp_tl} ) )[0] ) {
- $result = $self->add_result( $station, $s );
+ $result = $self->add_result( $station, $eva, $s );
if ($result) {
$result->set_unscheduled(1);
}
@@ -445,12 +807,14 @@ sub get_realtime {
my $msgid = $e_m->getAttribute('id');
my $ts = $e_m->getAttribute('ts');
- # 0 and 1 (with key "f") are related to canceled trains and
- # do not appear to hold information (or at least none we can access).
- # All observed cases of message ID 900 were related to bus
- # connections ("Anschlussbus wartet"). We can't access which bus
- # it refers to, so we don't show that either.
- if ( defined $value and $value > 1 and $value != 900 ) {
+ # 0 and 1 (with key "f") are related to canceled trains and
+ # do not appear to hold information (or at least none we can access).
+ # All observed cases of message ID 900 were related to bus
+ # connections ("Anschlussbus wartet"). We can't access which bus
+ # it refers to, so we don't show that either.
+ # ID 1000 is a generic free text message, which (as we lack access
+ # to the text itself) is not helpful either.
+ if ( defined $value and $value > 1 and $value < 100 ) {
$messages{$msgid} = [ $ts, $type, $value ];
}
}
@@ -469,9 +833,9 @@ sub get_realtime {
type => $e_ref->getAttribute('c'), # S/ICE/ERB/...
line_no => $e_ref->getAttribute('l'), # 1 -> S1, ...
- #unknown_t => $e_ref->getAttribute('t'), # p
- #unknown_o => $e_ref->getAttribute('o'), # owner: 03/80/R2/...
- # TODO ps='a' -> rerouted and normally unscheduled train?
+ #unknown_t => $e_ref->getAttribute('t'), # p
+ #unknown_o => $e_ref->getAttribute('o'), # owner: 03/80/R2/...
+ # TODO ps='a' -> rerouted and normally unscheduled train?
);
}
if ($e_ar) {
@@ -483,6 +847,7 @@ sub get_realtime {
sched_route_pre => $e_ar->getAttribute('ppth'),
status => $e_ar->getAttribute('cs'),
status_since => $e_ar->getAttribute('clt'),
+ arrival_hidden => $e_ar->getAttribute('hi'),
# TODO ps='a' -> rerouted and normally unscheduled train?
);
@@ -495,6 +860,7 @@ sub get_realtime {
route_post => $e_dp->getAttribute('cpth'),
sched_route_post => $e_dp->getAttribute('ppth'),
status => $e_dp->getAttribute('cs'),
+ departure_hidden => $e_dp->getAttribute('hi'),
);
}
@@ -514,7 +880,7 @@ sub get_result_by_train {
my ( $self, $type, $train_no ) = @_;
my $res = first { $_->type eq $type and $_->train_no eq $train_no }
- @{ $self->{results} };
+ @{ $self->{results} };
return $res;
}
@@ -596,14 +962,11 @@ Travel::Status::DE::IRIS - Interface to IRIS based web departure monitors.
=head1 SYNOPSIS
- use Travel::Status::DE::IRIS;
- use Travel::Status::DE::IRIS::Stations;
+Blocking variant:
- # Get station code for "Essen Hbf" (-> "EE")
- my $station = (Travel::Status::DE::IRIS::Stations::get_station_by_name(
- 'Essen Hbf'))[0][0];
+ use Travel::Status::DE::IRIS;
- my $status = Travel::Status::DE::IRIS->new(station => $station);
+ my $status = Travel::Status::DE::IRIS->new(station => "Essen Hbf");
for my $r ($status->results) {
printf(
"%s %s +%-3d %10s -> %s\n",
@@ -611,9 +974,29 @@ Travel::Status::DE::IRIS - Interface to IRIS based web departure monitors.
);
}
+Non-blocking variant (EXPERIMENTAL):
+
+ use Mojo::Promise;
+ use Mojo::UserAgent;
+ use Travel::Status::DE::IRIS;
+ use Travel::Status::DE::IRIS::Stations;
+
+ Travel::Status::DE::IRIS->new_p(station => "Essen Hbf",
+ promise => 'Mojo::Promise', user_agent => Mojo::UserAgent->new,
+ get_station => \&Travel::Status::DE::IRIS::Stations::get_station,
+ meta => Travel::Status::DE::IRIS::Stations::get_meta())->then(sub {
+ my ($status) = @_;
+ for my $r ($status->results) {
+ printf(
+ "%s %s +%-3d %10s -> %s\n",
+ $r->date, $r->time, $r->delay || 0, $r->line, $r->destination
+ );
+ }
+ })->wait;
+
=head1 VERSION
-version 1.28
+version 1.96
=head1 DESCRIPTION
@@ -643,11 +1026,32 @@ current date and time.
IRIS base url, defaults to C<< http://iris.noncd.db.de/iris-tts/timetable >>.
+=item B<keep_transfers> => I<bool>
+
+A train may change its ID and number at a station, indicating that although the
+previous logical train ends here, the physical train will continue its journey
+under a new number to a new destination. A notable example is the Berlin
+Ringbahn, which travels round and round from Berlin SE<uuml>dkreuz to Berlin
+SE<uuml>dkreuz. Each train number corresponds to a single revolution, but the
+actual trains just keep going.
+
+The IRIS backend returns two results for each transfer train: An arrival-only
+result using the old ID (linked to the new one) and a departure-only result
+using the new ID (linked to the old one). By default, this library merges these
+into a single result with both arrival and departure time. Train number, ID,
+and route are taken from the departure only. The original train ID and number
+are available using the B<old_train_id> and B<old_train_no> accessors.
+
+In case this is not desirable (e.g. because you intend to track a single
+train to its destination station and do not want to implement special cases
+for transfer trains), set B<keep_transfers> to a true value. In this case,
+backend data will be reported as-is and transfer trains will not be merged.
+
=item B<lookahead> => I<int>
Compute only results which are scheduled less than I<int> minutes in the
future.
-Default: 180 (3 hours).
+Default: 120 (2 hours).
Note that the DeutscheBahn IRIS backend only provides schedules up to four to
five hours into the future. So in most cases, setting this to a value above 240
@@ -702,11 +1106,54 @@ departures for all related stations.
=back
+=item my $promise = Travel::Status::DE::IRIS->new_p(I<%opt>) (B<EXPERIMENTAL>)
+
+Return a promise yielding a Travel::Status::DE::IRIS instance (C<< $status >>)
+on success, or an error message (same as C<< $status->errstr >>) on failure.
+This function is experimental and may be changed or remove without warning.
+
+In addition to the arguments of B<new>, the following mandatory arguments must
+be set:
+
+=over
+
+=item B<promise> => I<promises module>
+
+Promises implementation to use for internal promises as well as B<new_p> return
+value. Recommended: Mojo::Promise(3pm).
+
+=item B<get_station> => I<get_station ref>
+
+Reference to Travel::Status::DE::IRIS::Stations::get_station().
+
+=item B<meta> => I<meta dict>
+
+The dictionary returned by Travel::Status::DE::IRIS::Stations::get_meta().
+
+=item B<user_agent> => I<user agent>
+
+User agent instance to use for asynchronous requests. The object must support
+promises (i.e., it must implement a C<< get_p >> function). Recommended:
+Mojo::UserAgent(3pm).
+
+=back
+
=item $status->errstr
In case of a fatal HTTP request or IRIS error, returns a string describing it.
Returns undef otherwise.
+=item $status->related_stations
+
+Returns a list of hashes describing related stations whose
+arrivals/departures are included in B<results>. Only useful when setting
+B<with_related> to a true value, see its documentation above for details.
+
+Each hash contains the keys B<eva> (EVA number; often same as UIC station ID),
+B<name> (station name), and B<ds100> (station code). Note that stations
+returned by B<related_stations> are not necessarily known to
+Travel::Status::DE::IRIS::Stations(3pm).
+
=item $status->results
Returns a list of Travel::Status::DE::IRIS::Result(3pm) objects, each one describing
@@ -752,7 +1199,7 @@ L<https://github.com/derf/Travel-Status-DE-IRIS>
=head1 AUTHOR
-Copyright (C) 2013-2019 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt>
+Copyright (C) 2013-2024 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
=head1 LICENSE