diff options
Diffstat (limited to 'lib/DBInfoscreen/Controller/Stationboard.pm')
-rw-r--r-- | lib/DBInfoscreen/Controller/Stationboard.pm | 1831 |
1 files changed, 1260 insertions, 571 deletions
diff --git a/lib/DBInfoscreen/Controller/Stationboard.pm b/lib/DBInfoscreen/Controller/Stationboard.pm index 7f5b0d5..b64c661 100644 --- a/lib/DBInfoscreen/Controller/Stationboard.pm +++ b/lib/DBInfoscreen/Controller/Stationboard.pm @@ -1,6 +1,6 @@ package DBInfoscreen::Controller::Stationboard; -# Copyright (C) 2011-2020 Daniel Friesel +# Copyright (C) 2011-2020 Birte Kristina Friesel # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -8,30 +8,101 @@ use Mojo::Base 'Mojolicious::Controller'; use DateTime; use DateTime::Format::Strptime; -use Encode qw(decode encode); -use File::Slurp qw(read_file write_file); -use List::Util qw(max uniq); +use Encode qw(decode encode); +use File::Slurp qw(read_file write_file); +use List::Util qw(max uniq); +use List::UtilsBy qw(uniq_by); use List::MoreUtils qw(); -use Mojo::JSON qw(decode_json); +use Mojo::JSON qw(decode_json encode_json); use Mojo::Promise; +use Mojo::UserAgent; +use Travel::Status::DE::DBWagenreihung; +use Travel::Status::DE::HAFAS; use Travel::Status::DE::IRIS; use Travel::Status::DE::IRIS::Stations; use XML::LibXML; use utf8; -no if $] >= 5.018, warnings => 'experimental::smartmatch'; - my %default = ( mode => 'app', admode => 'deparr', ); +sub class_to_product { + my ( $self, $hafas ) = @_; + + my $bits = $hafas->get_active_service->{productbits}; + my $ret; + + for my $i ( 0 .. $#{$bits} ) { + $ret->{ 2**$i } + = ref( $bits->[$i] ) eq 'ARRAY' ? $bits->[$i][0] : $bits->[$i]; + } + + return $ret; +} + sub handle_no_results { - my ( $self, $station, $data ) = @_; + my ( $self, $station, $data, $hafas ) = @_; my $errstr = $data->{errstr}; + if ($hafas) { + $self->render_later; + my $service = 'DB'; + if ( $hafas ne '1' and Travel::Status::DE::HAFAS::get_service($hafas) ) + { + $service = $hafas; + } + Travel::Status::DE::HAFAS->new_p( + locationSearch => $station, + service => $service, + promise => 'Mojo::Promise', + user_agent => $self->ua, + )->then( + sub { + my ($status) = @_; + my @candidates = $status->results; + @candidates = map { [ $_->name, $_->eva ] } @candidates; + if ( @candidates == 1 and $candidates[0][0] ne $station ) { + my $s = $candidates[0][0]; + my $params = $self->req->params->to_string; + $self->redirect_to("/${s}?${params}"); + return; + } + for my $candidate (@candidates) { + $candidate->[0] =~ s{[&]#x0028;}{(}g; + $candidate->[0] =~ s{[&]#x0029;}{)}g; + } + my $err; + if ( not $errstr =~ m{LOCATION} ) { + $err = $errstr; + } + $self->render( + 'landingpage', + error => $err, + stationlist => \@candidates, + hide_opts => 0, + status => $data->{status} // 300, + ); + return; + } + )->catch( + sub { + my ($err) = @_; + $self->render( + 'landingpage', + error => ( $err // "Keine Abfahrten an '$station'" ), + hide_opts => 0, + status => $data->{status} // 500, + ); + return; + } + )->wait; + return; + } + my @candidates = map { [ $_->[1], $_->[0] ] } Travel::Status::DE::IRIS::Stations::get_station($station); if ( @@ -45,7 +116,7 @@ sub handle_no_results { 'landingpage', stationlist => \@candidates, hide_opts => 0, - status => 300, + status => $data->{status} // 300, ); return; } @@ -55,14 +126,16 @@ sub handle_no_results { 'landingpage', error => ( $errstr // "Keine Abfahrten an '$station'" ) . '. Das von DBF genutzte IRIS-Backend unterstützt im Regelfall nur innerdeutsche Zugfahrten.', - hide_opts => 0 + hide_opts => 0, + status => $data->{status} // 200, ); return; } $self->render( 'landingpage', error => ( $errstr // "Keine Abfahrten an '$station'" ), - hide_opts => 0 + hide_opts => 0, + status => $data->{status} // 404, ); return; } @@ -76,13 +149,10 @@ sub handle_no_results_json { $self->res->headers->access_control_allow_origin(q{*}); my $json; if ($errstr) { - $json = $self->render_to_string( - json => { - api_version => $api_version, - version => $self->config->{version}, - error => $errstr, - } - ); + $json = { + api_version => $api_version, + error => $errstr, + }; } else { my @candidates = map { { code => $_->[0], name => $_->[1] } } @@ -90,35 +160,30 @@ sub handle_no_results_json { if ( @candidates > 1 or ( @candidates == 1 and $candidates[0]{code} ne $station ) ) { - $json = $self->render_to_string( - json => { - api_version => $api_version, - version => $self->config->{version}, - error => 'ambiguous station code/name', - candidates => \@candidates, - } - ); + $json = { + api_version => $api_version, + error => 'ambiguous station code/name', + candidates => \@candidates, + }; } else { - $json = $self->render_to_string( - json => { - api_version => $api_version, - version => $self->config->{version}, - error => ( $errstr // "Got no results for '$station'" ) - } - ); + $json = { + api_version => $api_version, + error => ( $errstr // "Got no results for '$station'" ) + }; } } if ($callback) { + $json = $self->render_to_string( json => $json ); $self->render( data => "$callback($json);", - format => 'json' + format => 'json', ); } else { $self->render( - data => $json, - format => 'json' + json => $json, + status => $data->{status} // 300, ); } return; @@ -166,7 +231,9 @@ sub result_has_train_type { sub result_has_via { my ( $result, $via ) = @_; - my @route = $result->route_post; + my @route + = $result->can('route_post') ? $result->route_post : map { $_->loc->name } + $result->route; my $eq_result = List::MoreUtils::any { lc eq lc($via) } @route; @@ -191,22 +258,111 @@ sub result_has_via { } sub log_api_access { + my ($suffix) = @_; + $suffix //= q{}; + + my $file = "$ENV{DBFAKEDISPLAY_STATS}${suffix}"; my $counter = 1; - if ( -r $ENV{DBFAKEDISPLAY_STATS} ) { - $counter = read_file( $ENV{DBFAKEDISPLAY_STATS} ) + 1; + if ( -r $file ) { + $counter = read_file($file) + 1; } - write_file( $ENV{DBFAKEDISPLAY_STATS}, $counter ); + write_file( $file, $counter ); return; } -sub get_results_for { - my ( $station, %opt ) = @_; +sub json_route_diff { + my ( $self, $route, $sched_route ) = @_; + my @json_route; + my @route = @{$route}; + my @sched_route = @{$sched_route}; + + my $route_idx = 0; + my $sched_idx = 0; + + while ( $route_idx <= $#route and $sched_idx <= $#sched_route ) { + if ( $route[$route_idx] eq $sched_route[$sched_idx] ) { + push( @json_route, { name => $route[$route_idx] } ); + $route_idx++; + $sched_idx++; + } + + # this branch is inefficient, but won't be taken frequently + elsif ( + not( + List::MoreUtils::any { $route[$route_idx] eq $_ } + @sched_route + ) + ) + { + push( + @json_route, + { + name => $route[$route_idx], + isAdditional => 1 + } + ); + $route_idx++; + } + else { + push( + @json_route, + { + name => $sched_route[$sched_idx], + isCancelled => 1 + } + ); + $sched_idx++; + } + } + while ( $route_idx <= $#route ) { + push( + @json_route, + { + name => $route[$route_idx], + isAdditional => 1, + isCancelled => 0 + } + ); + $route_idx++; + } + while ( $sched_idx <= $#sched_route ) { + push( + @json_route, + { + name => $sched_route[$sched_idx], + isAdditional => 0, + isCancelled => 1 + } + ); + $sched_idx++; + } + return @json_route; +} + +sub get_results_p { + my ( $self, $station, %opt ) = @_; my $data; - # Cache::File has UTF-8 problems, so strip it (and any other potentially - # problematic chars). - my $cache_str = $station; - $cache_str =~ tr{[0-9a-zA-Z -]}{}cd; + if ( $opt{hafas} ) { + my $service = 'DB'; + if ( $opt{hafas} ne '1' + and Travel::Status::DE::HAFAS::get_service( $opt{hafas} ) ) + { + $service = $opt{hafas}; + } + return Travel::Status::DE::HAFAS->new_p( + service => $service, + station => $station, + arrivals => $opt{arrivals}, + cache => $opt{cache_iris_rt}, + lwp_options => { + timeout => 10, + agent => 'dbf.finalrewind.org/2' + }, + promise => 'Mojo::Promise', + user_agent => $self->ua, + ); + } if ( $ENV{DBFAKEDISPLAY_STATS} ) { log_api_access(); @@ -217,9 +373,15 @@ sub get_results_for { # if we have an exact match. Ask the backend otherwise. my @station_matches = Travel::Status::DE::IRIS::Stations::get_station($station); + + # Requests with EVA codes can be handled even if we do not know about them. + if ( @station_matches != 1 and $station =~ m{^\d+$} ) { + @station_matches = ( [ undef, undef, $station ] ); + } + if ( @station_matches == 1 ) { $station = $station_matches[0][2]; - my $status = Travel::Status::DE::IRIS->new( + return Travel::Status::DE::IRIS->new_p( iris_base => $ENV{DBFAKEDISPLAY_IRIS_BASE}, station => $station, main_cache => $opt{cache_iris_main}, @@ -230,31 +392,19 @@ sub get_results_for { timeout => 10, agent => 'dbf.finalrewind.org/2' }, + 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(), %opt ); - $data = { - results => [ $status->results ], - errstr => $status->errstr, - station_ds100 => - ( $status->station ? $status->station->{ds100} : undef ), - station_name => - ( $status->station ? $status->station->{name} : $station ), - }; } elsif ( @station_matches > 1 ) { - $data = { - results => [], - errstr => 'Ambiguous station name', - }; + return Mojo::Promise->reject('Ambiguous station name'); } else { - $data = { - results => [], - errstr => 'Unknown station name', - }; + return Mojo::Promise->reject('Unknown station name'); } - - return $data; } sub handle_request { @@ -262,11 +412,13 @@ sub handle_request { my $station = $self->stash('station'); my $template = $self->param('mode') // 'app'; + my $hafas = $self->param('hafas'); my $with_related = !$self->param('no_related'); my %opt = ( cache_iris_main => $self->app->cache_iris_main, cache_iris_rt => $self->app->cache_iris_rt, - lookahead => $self->config->{lookahead} + lookahead => $self->config->{lookahead}, + hafas => $hafas, ); if ( $self->param('past') ) { @@ -275,13 +427,22 @@ sub handle_request { $opt{lookahead} += 60; } + if ( $self->param('admode') and $self->param('admode') eq 'arr' ) { + $opt{arrivals} = 1; + } + my $api_version = $Travel::Status::DE::IRIS::VERSION; $self->stash( departures => [] ); $self->stash( title => 'DBF' ); - $self->stash( version => $self->config->{version} ); - if ( not( $template ~~ [qw[app infoscreen json multi single text]] ) ) { + if ( + not( + List::MoreUtils::any { $template eq $_ } + (qw(app infoscreen json multi single text)) + ) + ) + { $template = 'app'; } @@ -308,7 +469,12 @@ sub handle_request { # (or used by) marudor.de, it was renamed to 'json'. Many clients won't # notice this for year to come, so we make sure mode=marudor still works as # intended. - if ( $template eq 'marudor' ) { + if ( + $template eq 'marudor' + or ( $self->req->headers->accept + and $self->req->headers->accept eq 'application/json' ) + ) + { $template = 'json'; } @@ -329,26 +495,69 @@ sub handle_request { if ( $self->param('train') and not $opt{datetime} ) { - # request results from twenty minutes ago to avoid train details suddenly - # becoming unavailable when its scheduled departure is reached. + # request results from twenty minutes ago to avoid train details suddenly + # becoming unavailable when its scheduled departure is reached. $opt{datetime} = DateTime->now( time_zone => 'Europe/Berlin' ) ->subtract( minutes => 20 ); $opt{lookahead} = $self->config->{lookahead} + 20; } - my $data = get_results_for( $station, %opt ); - - if ( not @{ $data->{results} } and $template eq 'json' ) { - $self->handle_no_results_json( $station, $data, $api_version ); - return; - } - - if ( not @{ $data->{results} } ) { - $self->handle_no_results( $station, $data ); - return; - } + $self->render_later; - $self->handle_result($data); + $self->get_results_p( $station, %opt )->then( + sub { + my ($status) = @_; + my $data = { + results => [ $status->results ], + hafas => $hafas ? $status : undef, + station_ds100 => + ( $status->station ? $status->station->{ds100} : undef ), + station_eva => ( + $status->station + ? ( $status->station->{uic} // $status->station->{eva} ) + : undef + ), + station_evas => + ( $status->station ? $status->station->{evas} : [] ), + station_name => + ( $status->station ? $status->station->{name} : $station ), + }; + + if ( not @{ $data->{results} } and $template eq 'json' ) { + $self->handle_no_results_json( $station, $data, $api_version ); + return; + } + if ( not @{ $data->{results} } ) { + $self->handle_no_results( $station, $data, $hafas ); + return; + } + $self->handle_result($data); + } + )->catch( + sub { + my ($err) = @_; + if ( $template eq 'json' ) { + $self->handle_no_results_json( + $station, + { + errstr => $err, + status => ( $err =~ m{Ambiguous|LOCATION} ? 300 : 500 ), + }, + $api_version + ); + return; + } + $self->handle_no_results( + $station, + { + errstr => $err, + status => ( $err =~ m{Ambiguous|LOCATION} ? 300 : 500 ), + }, + $hafas + ); + return; + } + )->wait; } sub filter_results { @@ -439,7 +648,7 @@ sub format_iris_result_info { if ( $template ne 'json' ) { push( @{$moreinfo}, - [ 'Außerplanmäßiger Halt in', $additional_line ] + [ 'Außerplanmäßiger Halt in', { text => $additional_line } ] ); } } @@ -449,7 +658,7 @@ sub format_iris_result_info { $info = 'Ohne Halt in: ' . $cancel_line . ( $info ? ' +++ ' : q{} ) . $info; if ( $template ne 'json' ) { - push( @{$moreinfo}, [ 'Ohne Halt in', $cancel_line ] ); + push( @{$moreinfo}, [ 'Ohne Halt in', { text => $cancel_line } ] ); } } @@ -461,19 +670,21 @@ sub format_iris_result_info { sub render_train { my ( $self, $result, $departure, $station_name, $template ) = @_; - $departure->{links} = []; - $departure->{route_pre_diff} = [ - $self->json_route_diff( - [ $result->route_pre ], - [ $result->sched_route_pre ] - ) - ]; - $departure->{route_post_diff} = [ - $self->json_route_diff( - [ $result->route_post ], - [ $result->sched_route_post ] - ) - ]; + $departure->{links} = []; + if ( $result->can('route_pre') ) { + $departure->{route_pre_diff} = [ + $self->json_route_diff( + [ $result->route_pre ], + [ $result->sched_route_pre ] + ) + ]; + $departure->{route_post_diff} = [ + $self->json_route_diff( + [ $result->route_post ], + [ $result->sched_route_post ] + ) + ]; + } if ( not $result->has_realtime ) { my $now = DateTime->now( time_zone => 'Europe/Berlin' ); @@ -486,35 +697,109 @@ sub render_train { } my $linetype = 'bahn'; - my @classes = $result->classes; - if ( @classes == 0 ) { - $linetype = 'ext'; - } - elsif ( grep { $_ eq 'S' } @classes ) { - $linetype = 'sbahn'; + + if ( $result->can('classes') ) { + my @classes = $result->classes; + if ( @classes == 0 ) { + $linetype = 'ext'; + } + elsif ( grep { $_ eq 'S' } @classes ) { + $linetype = 'sbahn'; + } + elsif ( grep { $_ eq 'F' } @classes ) { + $linetype = 'fern'; + } } - elsif ( grep { $_ eq 'F' } @classes ) { - $linetype = 'fern'; + elsif ( $result->can('class') ) { + if ( $result->class <= 2 ) { + $linetype = 'fern'; + } + elsif ( $result->class == 16 ) { + $linetype = 'sbahn'; + } + elsif ( $result->class == 32 ) { + $linetype = 'bus'; + } + elsif ( $result->class == 128 ) { + $linetype = 'ubahn'; + } + elsif ( $result->class == 256 ) { + $linetype = 'tram'; + } } $self->render_later; my $wagonorder_req = Mojo::Promise->new; - my $utilization_req = Mojo::Promise->new; my $occupancy_req = Mojo::Promise->new; my $stationinfo_req = Mojo::Promise->new; my $route_req = Mojo::Promise->new; - my @requests = ( - $wagonorder_req, $utilization_req, $occupancy_req, - $stationinfo_req, $route_req - ); + my @requests + = ( $wagonorder_req, $occupancy_req, $stationinfo_req, $route_req ); if ( $departure->{wr_link} ) { - $self->wagonorder->is_available_p( $result, $departure->{wr_link} ) + $self->wagonorder->get_p( $result->train_no, $departure->{wr_link} ) ->then( sub { - # great! + my ($wr_json) = @_; + eval { + my $wr + = Travel::Status::DE::DBWagenreihung->new( + from_json => $wr_json ); + $departure->{wr} = $wr; + $departure->{wr_text} = join( q{ • }, + map { $_->desc_short } + grep { $_->desc_short } $wr->groups ); + my $first = 0; + for my $group ( $wr->groups ) { + my $had_entry = 0; + for my $wagon ( $group->wagons ) { + if ( + not( $wagon->is_locomotive + or $wagon->is_powercar ) + ) + { + my $class; + if ($first) { + push( + @{ $departure->{wr_preview} }, + [ '•', 'meta' ] + ); + $first = 0; + } + my $entry; + if ( $wagon->is_closed ) { + $entry = 'X'; + $class = 'closed'; + } + else { + $entry = $wagon->number + || ( + $wagon->type =~ m{AB} ? '½' + : $wagon->type =~ m{A} ? '1.' + : $wagon->type =~ m{B} ? '2.' + : $wagon->type + ); + } + if ( + $group->train_no ne $departure->{train_no} ) + { + $class = 'otherno'; + } + push( + @{ $departure->{wr_preview} }, + [ $entry, $class ] + ); + $had_entry = 1; + } + } + if ($had_entry) { + $first = 1; + } + } + }; + $departure->{wr_text} ||= 'Wagen'; return; }, sub { @@ -527,33 +812,12 @@ sub render_train { return; } )->wait; - - # Looks like utilization data is only available for long-distance trains - # – and the few regional trains which also have wagon order data (e.g. - # around Stuttgart). Funky. - $self->marudor->get_train_utilization( train => $result )->then( - sub { - my ( $first, $second ) = @_; - $departure->{utilization} = [ $first, $second ]; - return; - }, - sub { - $departure->{utilization} = undef; - return; - } - )->finally( - sub { - $utilization_req->resolve; - return; - } - )->wait; } else { $wagonorder_req->resolve; - $utilization_req->resolve; } - $self->marudor->get_efa_occupancy( + $self->efa->get_efa_occupancy( eva => $result->station_uic, train_no => $result->train_no )->then( @@ -609,10 +873,11 @@ sub render_train { } if ($direction) { - $departure->{direction} = $direction; + $departure->{wr_direction} = $direction; + $departure->{wr_direction_num} = $direction eq 'l' ? 0 : 100; } elsif ( $platform_info->{direction} ) { - $departure->{direction} = 'a' . $platform_info->{direction}; + $departure->{wr_direction} = 'a' . $platform_info->{direction}; } return; @@ -628,112 +893,88 @@ sub render_train { } )->wait; - $self->hafas->get_route_timestamps_p( train => $result )->then( - sub { - my ( $route_ts, $route_info, $trainsearch ) = @_; + my %opt = ( train => $result ); - $departure->{trip_id} = $trainsearch->{trip_id}; + #if ( $self->languages =~ m{^en} ) { + # $opt{language} = 'en'; + #} - # If a train number changes on the way, IRIS routes are incomplete, - # whereas HAFAS data has all stops -> merge HAFAS stops into IRIS - # stops. This is a rare case, one point where it can be observed is - # the TGV service at Frankfurt/Karlsruhe/Mannheim. - if ( $route_info - and my @hafas_stations = @{ $route_info->{stations} // [] } ) - { - if ( my @iris_stations = @{ $departure->{route_pre_diff} } ) { - my @missing_pre; - for my $station (@hafas_stations) { - if ( - List::MoreUtils::any { $_->{name} eq $station } - @iris_stations - ) - { - unshift( - @{ $departure->{route_pre_diff} }, - @missing_pre - ); - last; + $self->hafas->get_route_p(%opt)->then( + sub { + my ( $route, $journey ) = @_; + + $departure->{trip_id} = $journey->id; + $departure->{operators} = [ $journey->operators ]; + $departure->{date} = $route->[0]{sched_dep} // $route->[0]{dep}; + + # Use HAFAS route as source of truth; ignore IRIS data + $departure->{route_pre_diff} = []; + $departure->{route_post_diff} = $route; + my $split; + for my $i ( 0 .. $#{ $departure->{route_post_diff} } ) { + if ( $departure->{route_post_diff}[$i]{name} eq $station_name ) + { + $split = $i; + if ( my $load = $route->[$i]{load} ) { + if ( %{$load} ) { + $departure->{utilization} + = [ $load->{FIRST}, $load->{SECOND} ]; } - push( - @missing_pre, - { - name => $station, - hafas => 1 - } - ); } + $departure->{tz_offset} = $route->[$i]{tz_offset}; + $departure->{local_dt_da} = $route->[$i]{local_dt_da}; + $departure->{local_sched_arr} + = $route->[$i]{local_sched_arr}; + $departure->{local_sched_dep} + = $route->[$i]{local_sched_dep}; + $departure->{is_annotated} = $route->[$i]{is_annotated}; + $departure->{prod_name} = $route->[$i]{prod_name}; + $departure->{direction} = $route->[$i]{direction}; + $departure->{operator} = $route->[$i]{operator}; + last; } - if ( my @iris_stations = @{ $departure->{route_post_diff} } ) { - my @missing_post; - for my $station ( reverse @hafas_stations ) { - if ( - List::MoreUtils::any { $_->{name} eq $station } - @iris_stations - ) - { - push( - @{ $departure->{route_post_diff} }, - @missing_post - ); - last; - } - unshift( - @missing_post, - { - name => $station, - hafas => 1 - } - ); - } + } + + if ( defined $split ) { + for my $i ( 0 .. $split - 1 ) { + push( + @{ $departure->{route_pre_diff} }, + shift( @{ $departure->{route_post_diff} } ) + ); } + + # remove entry for $station_name + shift( @{ $departure->{route_post_diff} } ); } - if ($route_ts) { - if ( $route_ts->{ $result->station }{rt_bogus} ) { - #$departure->{missing_realtime} = 1; + my @him_messages; + my @him_details; + for my $message ( $journey->messages ) { + if ( $message->code ) { + push( @him_details, + [ $message->short // q{}, { text => $message->text } ] + ); } - for my $elem ( - @{ $departure->{route_pre_diff} }, - @{ $departure->{route_post_diff} } - ) - { - for my $key ( keys %{ $route_ts->{ $elem->{name} } // {} } ) - { - $elem->{$key} = $route_ts->{ $elem->{name} }{$key}; - } - if ( $elem->{rt_bogus} ) { - $departure->{partially_missing_realtime} = 1; - } + else { + push( @him_messages, + [ $message->short // q{}, { text => $message->text } ] + ); } } - if ( $route_info and @{ $route_info->{messages} // [] } ) { - my $him = $route_info->{messages}; - my @him_messages; - $departure->{messages}{him} = $him; - for my $message ( @{$him} ) { - if ( $message->{display} ) { - push( @him_messages, - [ $message->{header}, $message->{lead} ] ); - if ( $message->{lead} =~ m{zuginfo.nrw/?\?msg=(\d+)} ) { - push( - @{ $departure->{links} }, - [ - "Großstörung", - "https://zuginfo.nrw/?msg=$1" - ] - ); - } - } + for my $m (@him_messages) { + if ( $m->[0] =~ s{: Information.}{:} ) { + $m->[1]{icon} = 'info_outline'; + } + elsif ( $m->[0] =~ s{: Störung.}{: } ) { + $m->[1]{icon} = 'warning'; } - for my $message ( @{ $departure->{moreinfo} // [] } ) { - my $m = $message->[1]; - @him_messages - = grep { $_->[0] !~ m{Information\. $m\.$} } - @him_messages; + elsif ( $m->[0] =~ s{: Bauarbeiten.}{: } ) { + $m->[1]{icon} = 'build'; } - unshift( @{ $departure->{moreinfo} }, @him_messages ); + $m->[0] =~ s{(?!<)->}{ → }; } + unshift( @{ $departure->{moreinfo} }, @him_messages ); + unshift( @{ $departure->{details} }, @him_details ); } )->catch( sub { @@ -746,59 +987,45 @@ sub render_train { } )->wait; - # currently useless due to lack of Open Data - if ( 0 and $self->param('detailed') ) { - my $cycle_req = Mojo::Promise->new; - push( @requests, $cycle_req ); - $self->wagonorder->has_cycle_p( $result->train_no )->then( - sub { - $departure->{has_cycle} = 1; - } - )->catch( - sub { - # nop - } - )->finally( - sub { - $cycle_req->resolve; - return; - } - )->wait; - $departure->{composition} - = $self->app->train_details_db->{ $departure->{train_no} }; - my @cycle_from; - my @cycle_to; - for my $cycle ( values %{ $departure->{composition}->{cycle} // {} } ) { - push( @cycle_from, @{ $cycle->{from} // [] } ); - push( @cycle_to, @{ $cycle->{to} // [] } ); - } - @cycle_from = sort { $a <=> $b } uniq @cycle_from; - @cycle_to = sort { $a <=> $b } uniq @cycle_to; - $departure->{cycle_from} - = [ map { [ $_, $self->app->train_details_db->{$_} ] } @cycle_from ]; - $departure->{cycle_to} - = [ map { [ $_, $self->app->train_details_db->{$_} ] } @cycle_to ]; - } - # Defer rendering until all requests have completed Mojo::Promise->all(@requests)->then( sub { - $self->render( - $template // '_train_details', - departure => $departure, - linetype => $linetype, - icetype => $self->app->ice_type_map->{ $departure->{train_no} }, - details => $departure->{composition} // {}, - dt_now => DateTime->now( time_zone => 'Europe/Berlin' ), - station_name => $station_name, - nav_link => - $self->url_for( 'station', station => $station_name ) - ->query( { detailed => $self->param('detailed') } ), + $self->respond_to( + json => { + json => { + departure => $departure, + station_name => $station_name, + }, + }, + any => { + template => $template // '_train_details', + description => sprintf( + '%s %s%s%s nach %s', + $departure->{train_type}, + $departure->{train_line} // $departure->{train_no}, + $departure->{origin} ? ' von ' : q{}, + $departure->{origin} // q{}, + $departure->{destination} // 'unbekannt' + ), + departure => $departure, + linetype => $linetype, + dt_now => DateTime->now( time_zone => 'Europe/Berlin' ), + station_name => $station_name, + nav_link => + $self->url_for( 'station', station => $station_name ) + ->query( + { + detailed => $self->param('detailed'), + hafas => $self->param('hafas') + } + ), + }, ); } )->wait; } +# /z/:train/*station sub station_train_details { my ($self) = @_; my $train_no = $self->stash('train'); @@ -808,6 +1035,10 @@ sub station_train_details { delete $self->stash->{layout}; } + if ( $station =~ s{ [.] json $ }{}x ) { + $self->stash( format => 'json' ); + } + my %opt = ( cache_iris_main => $self->app->cache_iris_main, cache_iris_rt => $self->app->cache_iris_rt, @@ -830,81 +1061,116 @@ sub station_train_details { $opt{lookahead} = $self->config->{lookahead} + 20; } - my $data = get_results_for( $station, %opt ); - my $errstr = $data->{errstr}; - - if ( not @{ $data->{results} } ) { - $self->render( - 'landingpage', - error => "Keine Abfahrt von $train_no in $station gefunden", - status => 404, - ); - return; + # Berlin Hbf exists twice: + # - BLS / 8011160 + # - BL / 8098160 (formerly "Berlin Hbf (tief)") + # Right now DBF assumes that station name -> EVA / DS100 is a unique map. + # This is not the case. Work around it here until dbf has been adjusted + # properly. + if ( $station eq 'Berlin Hbf' ) { + $opt{with_related} = 1; } - my ($result) - = grep { result_is_train( $_, $train_no ) } @{ $data->{results} }; - - if ( not $result ) { - $self->render( - 'landingpage', - error => "Keine Abfahrt von $train_no in $station gefunden", - status => 404, - ); - return; - } + $self->render_later; - my ( $info, $moreinfo ) = $self->format_iris_result_info( 'app', $result ); - - my $result_info = { - sched_arrival => $result->sched_arrival - ? $result->sched_arrival->strftime('%H:%M') - : undef, - sched_departure => $result->sched_departure - ? $result->sched_departure->strftime('%H:%M') - : undef, - arrival => $result->arrival ? $result->arrival->strftime('%H:%M') - : undef, - departure => $result->departure ? $result->departure->strftime('%H:%M') - : undef, - train_type => $result->type // '', - train_line => $result->line_no, - train_no => $result->train_no, - destination => $result->destination, - origin => $result->origin, - platform => $result->platform, - scheduled_platform => $result->sched_platform, - is_cancelled => $result->is_cancelled, - departure_is_cancelled => $result->departure_is_cancelled, - arrival_is_cancelled => $result->arrival_is_cancelled, - moreinfo => $moreinfo, - delay => $result->delay, - route_pre => [ $result->route_pre ], - route_post => [ $result->route_post ], - replaced_by => - [ map { $_->type . q{ } . $_->train_no } $result->replaced_by ], - replacement_for => - [ map { $_->type . q{ } . $_->train_no } $result->replacement_for ], - wr_link => $result->sched_departure - ? $result->sched_departure->strftime('%Y%m%d%H%M') - : undef, - }; + # Always performs an IRIS request + $self->get_results_p( $station, %opt )->then( + sub { + my ($status) = @_; + my ($result) + = grep { result_is_train( $_, $train_no ) } $status->results; - $self->stash( title => $data->{station_name} // $self->stash('station') ); - $self->stash( hide_opts => 1 ); + if ( not $result ) { + die("Train not found\n"); + } - $self->render_train( - $result, $result_info, - $data->{station_name} // $self->stash('station'), - $self->param('ajax') ? '_train_details' : 'train_details' - ); + my ( $info, $moreinfo ) + = $self->format_iris_result_info( 'app', $result ); + + my $result_info = { + sched_arrival => $result->sched_arrival + ? $result->sched_arrival->strftime('%H:%M') + : undef, + sched_departure => $result->sched_departure + ? $result->sched_departure->strftime('%H:%M') + : undef, + arrival => $result->arrival + ? $result->arrival->strftime('%H:%M') + : undef, + departure => $result->departure + ? $result->departure->strftime('%H:%M') + : undef, + arrival_hidden => $result->arrival_hidden, + departure_hidden => $result->departure_hidden, + train_type => $result->type // '', + train_line => $result->line_no, + train_no => $result->train_no, + destination => $result->destination, + origin => $result->origin, + platform => $result->platform, + scheduled_platform => $result->sched_platform, + is_cancelled => $result->is_cancelled, + departure_is_cancelled => $result->departure_is_cancelled, + arrival_is_cancelled => $result->arrival_is_cancelled, + moreinfo => $moreinfo, + delay => $result->delay, + arrival_delay => $result->arrival_delay, + departure_delay => $result->departure_delay, + route_pre => [ $result->route_pre ], + route_post => [ $result->route_post ], + replaced_by => [ + map { $_->type . q{ } . $_->train_no } $result->replaced_by + ], + replacement_for => [ + map { $_->type . q{ } . $_->train_no } + $result->replacement_for + ], + wr_link => $result->sched_departure + ? $result->sched_departure->strftime('%Y%m%d%H%M') + : undef, + eva => $result->station_uic, + start => $result->start, + }; + + $self->stash( title => $status->station->{name} + // $self->stash('station') ); + $self->stash( hide_opts => 1 ); + + $self->render_train( + $result, + $result_info, + $status->station->{name} // $self->stash('station'), + $self->param('ajax') ? '_train_details' : 'train_details' + ); + } + )->catch( + sub { + my ($errstr) = @_; + $self->respond_to( + json => { + json => { + error => +"Keine Abfahrt von $train_no in $station gefunden: $errstr", + }, + status => 404, + }, + any => { + template => 'landingpage', + error => +"Keine Abfahrt von $train_no in $station gefunden: $errstr", + status => 404, + }, + ); + return; + } + )->wait; } +# /z/:train sub train_details { my ($self) = @_; - my $train = $self->stash('train'); - - my ( $train_type, $train_no ) = ( $train =~ m{ ^ (\S+) \s+ (.*) $ }x ); + my $train = $self->stash('train'); + my $hafas = $self->param('hafas'); # TODO error handling @@ -912,16 +1178,13 @@ sub train_details { delete $self->stash->{layout}; } - my $api_version = $Travel::Status::DE::IRIS::VERSION; - $self->stash( departures => [] ); $self->stash( title => 'DBF' ); - $self->stash( version => $self->config->{version} ); my $res = { - train_type => $train_type, + train_type => undef, train_line => undef, - train_no => $train_no, + train_no => undef, route_pre_diff => [], route_post_diff => [], moreinfo => [], @@ -929,92 +1192,236 @@ sub train_details { replacement_for => [], }; - $self->stash( title => "${train_type} ${train_no}" ); - $self->stash( hide_opts => 1 ); + my %opt; + + if ( $train =~ m{[|]} ) { + $opt{trip_id} = $train; + } + else { + my ( $train_type, $train_no ) = ( $train =~ m{ ^ (\S+) \s+ (.*) $ }x ); + $res->{train_type} = $train_type; + $res->{train_no} = $train_no; + $self->stash( title => "${train_type} ${train_no}" ); + $opt{train_type} = $train_type; + $opt{train_no} = $train_no; + } + + my $service = 'DB'; + if ( $hafas + and $hafas ne '1' + and Travel::Status::DE::HAFAS::get_service($hafas) ) + { + $opt{service} = $hafas; + } + + #if ( $self->languages =~ m{^en} ) { + # $opt{language} = 'en'; + #} + if ( my $date = $self->param('date') ) { + if ( $date + =~ m{ ^ (?<day> \d{1,2} ) [.] (?<month> \d{1,2} ) [.] (?<year> \d{4})? $ }x + ) + { + $opt{datetime} = DateTime->now( time_zone => 'Europe/Berlin' ); + $opt{datetime}->set( + day => $+{day}, + month => $+{month} + ); + if ( $+{year} ) { + $opt{datetime}->set( year => $+{year} ); + } + } + } + + $self->stash( hide_opts => 1 ); $self->render_later; my $linetype = 'bahn'; - $self->hafas->get_route_timestamps_p( - train_req => "${train_type} $train_no" )->then( + $self->hafas->get_route_p(%opt)->then( sub { - my ( $route_ts, $route_info, $trainsearch ) = @_; + my ( $route, $journey, $hafas_obj ) = @_; - $res->{trip_id} = $trainsearch->{trip_id}; + $res->{trip_id} = $journey->id; + $res->{date} = $route->[0]{sched_dep} // $route->[0]{dep}; - if ( not defined $trainsearch->{trainClass} ) { - $linetype = 'ext'; - } - elsif ( $trainsearch->{trainClass} <= 2 ) { - $linetype = 'fern'; + my $product = $journey->product; + + if ( my $req_name = $self->param('highlight') ) { + if ( my $p = $journey->product_at($req_name) ) { + $product = $p; + } } - elsif ( $trainsearch->{trainClass} <= 8 ) { - $linetype = 'bahn'; + + my $train_type = $res->{train_type} = $product->type // q{}; + my $train_no = $res->{train_no} = $product->number // q{}; + $res->{train_line} = $product->line_no // q{}; + $self->stash( title => $train_type . ' ' + . ( $train_no || $res->{train_line} ) ); + + if ( not defined $product->class ) { + $linetype = 'ext'; } - elsif ( $trainsearch->{trainClass} <= 16 ) { - $linetype = 'sbahn'; + else { + my $prod + = $self->class_to_product($hafas_obj)->{ $product->class } + // q{}; + if ( $prod eq 'ice' or $prod eq 'ic_ec' ) { + $linetype = 'fern'; + } + elsif ( $prod eq 's' ) { + $linetype = 'sbahn'; + } + elsif ( $prod eq 'bus' ) { + $linetype = 'bus'; + } + elsif ( $prod eq 'u' ) { + $linetype = 'ubahn'; + } + elsif ( $prod eq 'tram' ) { + $linetype = 'tram'; + } } - $res->{origin} = $route_info->{stations}[0]; - $res->{destination} = $route_info->{stations}[-1]; + $res->{origin} = $journey->route_start; + $res->{destination} = $journey->route_end; + $res->{operators} = [ $journey->operators ]; - $res->{route_post_diff} - = [ map { { name => $_ } } @{ $route_info->{stations} } ]; + $res->{route_post_diff} = $route; - if ($route_ts) { - for my $elem ( @{ $res->{route_post_diff} } ) { - for my $key ( keys %{ $route_ts->{ $elem->{name} } // {} } ) - { - $elem->{$key} = $route_ts->{ $elem->{name} }{$key}; + if ( my $req_name = $self->param('highlight') ) { + my $split; + for my $i ( 0 .. $#{ $res->{route_post_diff} } ) { + if ( $res->{route_post_diff}[$i]{name} eq $req_name ) { + $split = $i; + last; } } + if ( defined $split ) { + $self->stash( station_name => $req_name ); + for my $i ( 0 .. $split - 1 ) { + push( + @{ $res->{route_pre_diff} }, + shift( @{ $res->{route_post_diff} } ) + ); + } + my $station_info = shift( @{ $res->{route_post_diff} } ); + $res->{eva} = $station_info->{eva}; + if ( $station_info->{sched_arr} ) { + $res->{sched_arrival} + = $station_info->{sched_arr}->strftime('%H:%M'); + } + if ( $station_info->{rt_arr} ) { + $res->{arrival} + = $station_info->{rt_arr}->strftime('%H:%M'); + } + if ( $station_info->{sched_dep} ) { + $res->{sched_departure} + = $station_info->{sched_dep}->strftime('%H:%M'); + } + if ( $station_info->{rt_dep} ) { + $res->{departure} + = $station_info->{rt_dep}->strftime('%H:%M'); + } + $res->{arrival_is_cancelled} + = $station_info->{arr_cancelled}; + $res->{departure_is_cancelled} + = $station_info->{dep_cancelled}; + $res->{is_cancelled} = $res->{arrival_is_cancelled} + || $res->{arrival_is_cancelled}; + $res->{tz_offset} = $station_info->{tz_offset}; + $res->{local_dt_da} = $station_info->{local_dt_da}; + $res->{local_sched_arr} = $station_info->{local_sched_arr}; + $res->{local_sched_dep} = $station_info->{local_sched_dep}; + $res->{is_annotated} = $station_info->{is_annotated}; + $res->{prod_name} = $station_info->{prod_name}; + $res->{direction} = $station_info->{direction}; + $res->{operator} = $station_info->{operator}; + $res->{platform} = $station_info->{platform}; + $res->{scheduled_platform} + = $station_info->{sched_platform}; + } } - if ( $route_info and @{ $route_info->{messages} // [] } ) { - my $him = $route_info->{messages}; - my @him_messages; - for my $message ( @{$him} ) { - if ( $message->{display} ) { - push( @him_messages, - [ $message->{header}, $message->{lead} ] ); - if ( $message->{lead} =~ m{zuginfo.nrw/?\?msg=(\d+)} ) { - push( - @{ $res->{links} }, - [ - "Großstörung", - "https://zuginfo.nrw/?msg=$1" - ] - ); - } - } + my @him_messages; + my @him_details; + for my $message ( $journey->messages ) { + if ( $message->code ) { + push( @him_details, + [ $message->short // q{}, { text => $message->text } ] + ); + } + else { + push( @him_messages, + [ $message->short // q{}, { text => $message->text } ] + ); + } + } + for my $m (@him_messages) { + if ( $m->[0] =~ s{: Information.}{:} ) { + $m->[1]{icon} = 'info_outline'; } + elsif ( $m->[0] =~ s{: Störung.}{: } ) { + $m->[1]{icon} = 'warning'; + } + elsif ( $m->[0] =~ s{: Bauarbeiten.}{: } ) { + $m->[1]{icon} = 'build'; + } + } + if (@him_messages) { $res->{moreinfo} = [@him_messages]; } + if (@him_details) { + $res->{details} = [@him_details]; + } - $self->render( - $self->param('ajax') ? '_train_details' : 'train_details', - departure => $res, - linetype => $linetype, - icetype => $self->app->ice_type_map->{ $res->{train_no} }, - details => {}, #$departure->{composition} // {}, - dt_now => DateTime->now( time_zone => 'Europe/Berlin' ), - - #station_name => "FIXME",#$station_name, + $self->respond_to( + json => { + json => { + journey => $journey, + }, + }, + any => { + template => $self->param('ajax') + ? '_train_details' + : 'train_details', + description => sprintf( + '%s %s%s%s nach %s', + $res->{train_type}, + $res->{train_line} // $res->{train_no}, + $res->{origin} ? ' von ' : q{}, + $res->{origin} // q{}, + $res->{destination} // 'unbekannt' + ), + departure => $res, + linetype => $linetype, + dt_now => DateTime->now( time_zone => 'Europe/Berlin' ), + }, ); } )->catch( sub { my ($e) = @_; if ($e) { - $self->render( - 'exception', - exception => $e, - snapshot => {} + $self->respond_to( + json => { + json => { + error => $e, + }, + status => 500, + }, + any => { + template => 'exception', + message => $e, + exception => undef, + snapshot => {}, + status => 500, + }, ); } else { - $self->render('not_found'); + $self->render( 'not_found', status => 404 ); } } )->wait; @@ -1027,15 +1434,18 @@ sub handle_result { my @departures; my @platforms = split( /,/, $self->param('platforms') // q{} ); - my $template = $self->param('mode') // 'app'; - my $hide_low_delay = $self->param('hidelowdelay') // 0; - my $hide_opts = $self->param('hide_opts') // 0; - my $show_realtime = $self->param('show_realtime') // 0; - my $show_details = $self->param('detailed') // 0; - my $admode = $self->param('admode') // 'deparr'; - my $apiver = $self->param('version') // 0; - my $callback = $self->param('callback'); - my $via = $self->param('via'); + my $template = $self->param('mode') // 'app'; + my $hide_low_delay = $self->param('hidelowdelay') // 0; + my $hide_opts = $self->param('hide_opts') // 0; + my $show_realtime = $self->param('rt') // $self->param('show_realtime') + // 0; + my $show_details = $self->param('detailed') // 0; + my $admode = $self->param('admode') // 'deparr'; + my $apiver = $self->param('version') // 0; + my $callback = $self->param('callback'); + my $via = $self->param('via'); + my $hafas = $self->param('hafas'); + my $hafas_obj = $data->{hafas}; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); @@ -1046,13 +1456,12 @@ sub handle_result { if ( $template eq 'single' ) { if ( not @platforms ) { for my $result (@results) { + my $num_part + = $self->numeric_platform_part( $result->platform ); if ( - not( $self->numeric_platform_part( $result->platform ) ~~ - \@platforms ) - ) + not( List::MoreUtils::any { $num_part eq $_ } @platforms ) ) { - push( @platforms, - $self->numeric_platform_part( $result->platform ) ); + push( @platforms, $num_part ); } } @platforms = sort { $a <=> $b } @platforms; @@ -1067,7 +1476,10 @@ sub handle_result { } if ($show_realtime) { - if ( $admode eq 'arr' ) { + if ($hafas) { + @results = sort { $a->datetime <=> $b->datetime } @results; + } + elsif ( $admode eq 'arr' ) { @results = sort { ( $a->arrival // $a->departure ) <=> ( $b->arrival // $b->departure ) @@ -1081,44 +1493,77 @@ sub handle_result { } } + my $class_to_product + = $hafas_obj ? $self->class_to_product($hafas_obj) : {}; + @results = $self->filter_results(@results); for my $result (@results) { my $platform = ( split( qr{ }, $result->platform // '' ) )[0]; my $delay = $result->delay; - if ( $admode eq 'arr' and not $result->arrival ) { + if ( $admode eq 'arr' and not $hafas and not $result->arrival ) { next; } - if ( $admode eq 'dep' + if ( $admode eq 'dep' + and not $hafas and not $result->departure ) { next; } - my ( $info, $moreinfo ) - = $self->format_iris_result_info( $template, $result ); + my ( $info, $moreinfo ); + if ( $result->can('replacement_for') ) { + ( $info, $moreinfo ) + = $self->format_iris_result_info( $template, $result ); + } - my $time = $result->time; + my $time + = $result->can('time') + ? $result->time + : $result->sched_datetime->strftime('%H:%M'); my $linetype = 'bahn'; - my @classes = $result->classes; - if ( @classes == 0 ) { - $linetype = 'ext'; - } - elsif ( grep { $_ eq 'S' } @classes ) { - $linetype = 'sbahn'; + if ( $result->can('classes') ) { + my @classes = $result->classes; + if ( @classes == 0 ) { + $linetype = 'ext'; + } + elsif ( grep { $_ eq 'S' } @classes ) { + $linetype = 'sbahn'; + } + elsif ( grep { $_ eq 'F' } @classes ) { + $linetype = 'fern'; + } } - elsif ( grep { $_ eq 'F' } @classes ) { - $linetype = 'fern'; + elsif ( $result->can('class') ) { + my $prod = $class_to_product->{ $result->class } // q{}; + if ( $prod eq 'ice' or $prod eq 'ic_ec' ) { + $linetype = 'fern'; + } + elsif ( $prod eq 's' ) { + $linetype = 'sbahn'; + } + elsif ( $prod eq 'bus' ) { + $linetype = 'bus'; + } + elsif ( $prod eq 'u' ) { + $linetype = 'ubahn'; + } + elsif ( $prod eq 'tram' ) { + $linetype = 'tram'; + } } # ->time defaults to dep, so we only need to overwrite $time # if we want arrival times - if ( $admode eq 'arr' ) { + if ( $admode eq 'arr' and not $hafas ) { $time = $result->sched_arrival->strftime('%H:%M'); } if ($show_realtime) { - if ( ( $admode eq 'arr' and $result->arrival ) + if ($hafas) { + $time = $result->datetime->strftime('%H:%M'); + } + elsif ( ( $admode eq 'arr' and $result->arrival ) or not $result->departure ) { $time = $result->arrival->strftime('%H:%M'); @@ -1138,8 +1583,14 @@ sub handle_result { } if ( $template eq 'json' ) { - my @json_route = $self->json_route_diff( [ $result->route ], - [ $result->sched_route ] ); + my @json_route; + if ( $result->can('sched_route') ) { + @json_route = $self->json_route_diff( [ $result->route ], + [ $result->sched_route ] ); + } + else { + @json_route = map { $_->TO_JSON } $result->route; + } if ( $apiver eq '1' or $apiver eq '2' ) { @@ -1155,29 +1606,136 @@ sub handle_result { return; } else { # apiver == 3 - my ( $delay_arr, $delay_dep, $sched_arr, $sched_dep ); - if ( $result->arrival ) { - $delay_arr = $result->arrival->subtract_datetime( - $result->sched_arrival )->in_units('minutes'); - } - if ( $result->departure ) { - $delay_dep = $result->departure->subtract_datetime( - $result->sched_departure )->in_units('minutes'); - } - if ( $result->sched_arrival ) { - $sched_arr = $result->sched_arrival->strftime('%H:%M'); + if ( $result->isa('Travel::Status::DE::IRIS::Result') ) { + my ( $delay_arr, $delay_dep, $sched_arr, $sched_dep ); + if ( $result->arrival ) { + $delay_arr = $result->arrival->subtract_datetime( + $result->sched_arrival )->in_units('minutes'); + } + if ( $result->departure ) { + $delay_dep = $result->departure->subtract_datetime( + $result->sched_departure )->in_units('minutes'); + } + if ( $result->sched_arrival ) { + $sched_arr = $result->sched_arrival->strftime('%H:%M'); + } + if ( $result->sched_departure ) { + $sched_dep + = $result->sched_departure->strftime('%H:%M'); + } + push( + @departures, + { + delayArrival => $delay_arr, + delayDeparture => $delay_dep, + destination => $result->destination, + isCancelled => $result->is_cancelled, + messages => { + delay => [ + map { + { + timestamp => $_->[0], + text => $_->[1] + } + } $result->delay_messages + ], + qos => [ + map { + { + timestamp => $_->[0], + text => $_->[1] + } + } $result->qos_messages + ], + }, + missingRealtime => ( + ( + not $result->has_realtime + and $result->start < $now + ) ? \1 : \0 + ), + platform => $result->platform, + route => \@json_route, + scheduledPlatform => $result->sched_platform, + scheduledArrival => $sched_arr, + scheduledDeparture => $sched_dep, + train => $result->train, + trainClasses => [ $result->classes ], + trainNumber => $result->train_no, + via => [ $result->route_interesting(3) ], + } + ); } - if ( $result->sched_departure ) { - $sched_dep = $result->sched_departure->strftime('%H:%M'); + else { + push( + @departures, + { + delay => $result->delay, + direction => $result->direction, + destination => $result->destination, + isCancelled => $result->is_cancelled, + messages => [ $result->messages ], + platform => $result->platform, + route => \@json_route, + scheduledPlatform => $result->sched_platform, + scheduledTime => $result->sched_datetime->epoch, + time => $result->datetime->epoch, + train => $result->line, + trainNumber => $result->number, + via => [ $result->route_interesting(3) ], + } + ); } + } + } + elsif ( $template eq 'text' ) { + push( + @departures, + [ + sprintf( '%5s %s%s', + $result->is_cancelled ? '--:--' : $time, + ( $delay and $delay > 0 ) ? q{+} : q{}, + $delay || q{} ), + $result->train, + $result->destination, + $platform // q{ } + ] + ); + } + else { + if ( $result->can('replacement_for') ) { push( @departures, { - delayArrival => $delay_arr, - delayDeparture => $delay_dep, - destination => $result->destination, - isCancelled => $result->is_cancelled, - messages => { + time => $time, + sched_arrival => $result->sched_arrival + ? $result->sched_arrival->strftime('%H:%M') + : undef, + sched_departure => $result->sched_departure + ? $result->sched_departure->strftime('%H:%M') + : undef, + arrival => $result->arrival + ? $result->arrival->strftime('%H:%M') + : undef, + departure => $result->departure + ? $result->departure->strftime('%H:%M') + : undef, + train => $result->train, + train_type => $result->type // '', + train_line => $result->line_no, + train_no => $result->train_no, + via => [ $result->route_interesting(3) ], + destination => $result->destination, + origin => $result->origin, + platform => $result->platform, + scheduled_platform => $result->sched_platform, + info => $info, + is_cancelled => $result->is_cancelled, + departure_is_cancelled => + $result->departure_is_cancelled, + arrival_is_cancelled => $result->arrival_is_cancelled, + linetype => $linetype, + messages => { delay => [ map { { @@ -1195,104 +1753,82 @@ sub handle_result { } $result->qos_messages ], }, - missingRealtime => ( - ( - not $result->has_realtime - and $result->start < $now - ) ? \1 : \0 + station => $result->station, + moreinfo => $moreinfo, + delay => $delay, + arrival_delay => $result->arrival_delay, + departure_delay => $result->departure_delay, + missing_realtime => ( + not $result->has_realtime + and $result->start < $now ? 1 : 0 ), - platform => $result->platform, - route => \@json_route, - scheduledPlatform => $result->sched_platform, - scheduledArrival => $sched_arr, - scheduledDeparture => $sched_dep, - train => $result->train, - trainClasses => [ $result->classes ], - trainNumber => $result->train_no, - via => [ $result->route_interesting(3) ], + route_pre => [ $result->route_pre ], + route_post => [ $result->route_post ], + additional_stops => [ $result->additional_stops ], + canceled_stops => [ $result->canceled_stops ], + replaced_by => [ + map { $_->type . q{ } . $_->train_no } + $result->replaced_by + ], + replacement_for => [ + map { $_->type . q{ } . $_->train_no } + $result->replacement_for + ], + wr_link => $result->sched_departure + ? $result->sched_departure->strftime('%Y%m%d%H%M') + : undef, } ); } - } - elsif ( $template eq 'text' ) { - push( - @departures, - [ - sprintf( '%5s %s%s', - $result->is_cancelled ? '--:--' : $time, - ( $delay and $delay > 0 ) ? q{+} : q{}, - $delay || q{} ), - $result->train, - $result->destination, - $platform // q{ } - ] - ); - } - else { - push( - @departures, - { - time => $time, - sched_arrival => $result->sched_arrival - ? $result->sched_arrival->strftime('%H:%M') - : undef, - sched_departure => $result->sched_departure - ? $result->sched_departure->strftime('%H:%M') - : undef, - arrival => $result->arrival - ? $result->arrival->strftime('%H:%M') - : undef, - departure => $result->departure - ? $result->departure->strftime('%H:%M') - : undef, - train => $result->train, - train_type => $result->type // '', - train_line => $result->line_no, - train_no => $result->train_no, - via => [ $result->route_interesting(3) ], - destination => $result->destination, - origin => $result->origin, - platform => $result->platform, - scheduled_platform => $result->sched_platform, - info => $info, - is_cancelled => $result->is_cancelled, - departure_is_cancelled => $result->departure_is_cancelled, - arrival_is_cancelled => $result->arrival_is_cancelled, - linetype => $linetype, - messages => { - delay => [ - map { { timestamp => $_->[0], text => $_->[1] } } - $result->delay_messages - ], - qos => [ - map { { timestamp => $_->[0], text => $_->[1] } } - $result->qos_messages - ], - }, - station => $result->station, - moreinfo => $moreinfo, - delay => $delay, - missing_realtime => ( - not $result->has_realtime - and $result->start < $now ? 1 : 0 - ), - route_pre => [ $result->route_pre ], - route_post => [ $result->route_post ], - additional_stops => [ $result->additional_stops ], - canceled_stops => [ $result->canceled_stops ], - replaced_by => [ - map { $_->type . q{ } . $_->train_no } - $result->replaced_by - ], - replacement_for => [ - map { $_->type . q{ } . $_->train_no } - $result->replacement_for - ], - wr_link => $result->sched_departure - ? $result->sched_departure->strftime('%Y%m%d%H%M') - : undef, + else { + my $city = q{}; + if ( $result->station =~ m{ , ([^,]+) $ }x ) { + $city = $1; } - ); + push( + @departures, + { + time => $time, + sched_departure => + ( $result->sched_datetime and $admode ne 'arr' ) + ? $result->sched_datetime->strftime('%H:%M') + : undef, + departure => + ( $result->rt_datetime and $admode ne 'arr' ) + ? $result->rt_datetime->strftime('%H:%M') + : undef, + train => $result->name, + train_type => q{}, + train_line => $result->line, + train_no => $result->number, + journey_id => $result->id, + via => [ + map { $_->loc->name =~ s{,\Q$city\E}{}r } + $result->route_interesting(3) + ], + destination => $result->route_end =~ s{,\Q$city\E}{}r, + origin => $result->route_end =~ s{,\Q$city\E}{}r, + platform => $result->platform, + scheduled_platform => $result->sched_platform, + info => $info, + is_cancelled => $result->is_cancelled, + linetype => $linetype, + station => $result->station, + moreinfo => $moreinfo, + delay => $delay, + replaced_by => [], + replacement_for => [], + route_pre => $admode eq 'arr' + ? [ map { $_->loc->name } $result->route ] + : [], + route_post => $admode eq 'arr' ? [] + : [ map { $_->loc->name } $result->route ], + wr_link => $result->sched_datetime + ? $result->sched_datetime->strftime('%Y%m%d%H%M') + : undef, + } + ); + } if ( $self->param('train') ) { $self->render_train( $result, $departures[-1], $data->{station_name} // $self->stash('station') ); @@ -1303,12 +1839,11 @@ sub handle_result { if ( $template eq 'json' ) { $self->res->headers->access_control_allow_origin(q{*}); - my $json = $self->render_to_string( - json => { - departures => \@departures, - } - ); + my $json = { + departures => \@departures, + }; if ($callback) { + $json = $self->render_to_string( json => $json ); $self->render( data => "$callback($json);", format => 'json' @@ -1316,8 +1851,7 @@ sub handle_result { } else { $self->render( - data => $json, - format => 'json' + json => $json, ); } } @@ -1340,10 +1874,36 @@ sub handle_result { } else { my $station_name = $data->{station_name} // $self->stash('station'); + my ( $api_link, $api_text, $api_icon ); + my $params = $self->req->params->clone; + $params->param( hafas => not $params->param('hafas') ); + if ( $params->param('hafas') ) { + if ( $data->{station_eva} >= 8100000 + and $data->{station_eva} < 8200000 ) + { + $params->param( hafas => 'ÖBB' ); + } + $api_link = '/' . $data->{station_eva} . '?' . $params->to_string; + $api_text = 'Auf Nahverkehr wechseln'; + $api_icon = 'train'; + } + else { + my $iris_eva = List::Util::min grep { $_ >= 1000000 } + @{ $data->{station_evas} // [] }; + if ($iris_eva) { + $api_link = '/' . $iris_eva . '?' . $params->to_string; + $api_text = 'Auf Bahnverkehr wechseln'; + $api_icon = 'directions'; + } + } $self->render( $template, + description => 'Abfahrtstafel ' + . ( $via ? "$station_name via $via" : $station_name ), + api_link => $api_link, + api_text => $api_text, + api_icon => $api_icon, departures => \@departures, - ice_type => $self->app->ice_type_map, station => $station_name, version => $self->config->{version}, title => $via ? "$station_name → $via" : $station_name, @@ -1356,8 +1916,13 @@ sub handle_result { or $template eq 'multi' ), force_mobile => ( $template eq 'app' ), - nav_link => $self->url_for( 'station', station => $station_name ) - ->query( { detailed => $self->param('detailed') } ), + nav_link => + $self->url_for( 'station', station => $station_name )->query( + { + detailed => $self->param('detailed'), + hafas => $self->param('hafas') + } + ), ); } return; @@ -1366,29 +1931,153 @@ sub handle_result { sub stations_by_coordinates { my $self = shift; - my $lon = $self->param('lon'); - my $lat = $self->param('lat'); + my $lon = $self->param('lon'); + my $lat = $self->param('lat'); + my $hafas = $self->param('hafas'); if ( not $lon or not $lat ) { $self->render( json => { error => 'Invalid lon/lat received' } ); + return; } - else { - my @candidates = map { - { - ds100 => $_->[0][0], - name => $_->[0][1], - eva => $_->[0][2], - lon => $_->[0][3], - lat => $_->[0][4], - distance => $_->[1], - } - } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon, - $lat, 10 ); - $self->render( - json => { - candidates => [@candidates], + + my $service = 'DB'; + if ( $hafas + and $hafas ne '1' + and Travel::Status::DE::HAFAS::get_service($hafas) ) + { + $service = $hafas; + } + + $self->render_later; + + my @iris = map { + { + ds100 => $_->[0][0], + name => $_->[0][1], + eva => $_->[0][2], + lon => $_->[0][3], + lat => $_->[0][4], + distance => $_->[1], + hafas => 0, + } + } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon, + $lat, 10 ); + + @iris = uniq_by { $_->{name} } @iris; + + Travel::Status::DE::HAFAS->new_p( + promise => 'Mojo::Promise', + user_agent => $self->ua, + service => $service, + geoSearch => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($hafas) = @_; + my @hafas = map { + { + name => $_->name, + eva => $_->eva, + distance => $_->distance_m / 1000, + hafas => $service, + } + } $hafas->results; + if ( @hafas > 10 ) { + @hafas = @hafas[ 0 .. 9 ]; } - ); + my @results = map { $_->[0] } + sort { $a->[1] <=> $b->[1] } + map { [ $_, $_->{distance} ] } ( @iris, @hafas ); + $self->render( + json => { + candidates => [@results], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [@iris], + warning => $err, + } + ); + } + )->wait; +} + +sub autocomplete { + my ($self) = @_; + + $self->res->headers->cache_control('max-age=31536000, immutable'); + + my $output = '$(function(){const stations='; + $output + .= encode_json( + [ map { $_->[1] } Travel::Status::DE::IRIS::Stations::get_stations() ] + ); + $output .= ";\n"; + $output + .= "\$('input.station').autocomplete({delay:0,minLength:3,source:stations});});\n"; + + $self->render( + format => 'js', + data => $output + ); +} + +sub redirect_to_station { + my ($self) = @_; + my $input = $self->param('input'); + my $params = $self->req->params; + + $params->remove('input'); + + for my $param (qw(platforms mode admode via)) { + if ( + not $params->param($param) + or ( exists $default{$param} + and $params->param($param) eq $default{$param} ) + ) + { + $params->remove($param); + } + } + + if ( $input =~ m{ ^ [a-zA-Z]{1,5} \s+ \d+ }x ) { + if ( $input =~ s{ \s* @ \s* (?<date> [0-9.]+) $ }{}x ) { + $params->param( date => $+{date} ); + } + elsif ( $input =~ s{ \s* [(] \s* (?<date> [0-9.]+) \s* [)] $ }{}x ) { + $params->param( date => $+{date} ); + } + $params = $params->to_string; + $self->redirect_to("/z/${input}?${params}"); + } + elsif ( $params->param('hafas') and $params->param('hafas') ne '1' ) { + $params = $params->to_string; + $self->redirect_to("/${input}?${params}"); + } + else { + my @candidates + = Travel::Status::DE::IRIS::Stations::get_station($input); + if ( + @candidates == 1 + and ( $input eq $candidates[0][0] + or lc($input) eq lc( $candidates[0][1] ) + or $input eq $candidates[0][2] ) + ) + { + $params->remove('hafas'); + } + else { + $params->param( hafas => 1 ); + } + $params = $params->to_string; + $self->redirect_to("/${input}?${params}"); } } |