diff options
Diffstat (limited to 'lib/Travelynx/Controller')
-rw-r--r-- | lib/Travelynx/Controller/Account.pm | 296 | ||||
-rwxr-xr-x | lib/Travelynx/Controller/Api.pm | 67 | ||||
-rw-r--r-- | lib/Travelynx/Controller/Passengerrights.pm | 7 | ||||
-rwxr-xr-x | lib/Travelynx/Controller/Profile.pm | 141 | ||||
-rw-r--r-- | lib/Travelynx/Controller/Static.pm | 6 | ||||
-rw-r--r-- | lib/Travelynx/Controller/Traewelling.pm | 2 | ||||
-rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 1197 |
7 files changed, 1370 insertions, 346 deletions
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index f1dc43e..bf1eac2 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1,11 +1,13 @@ package Travelynx::Controller::Account; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Controller'; use JSON; +use Math::Polygon; use Mojo::Util qw(xml_escape); use Text::Markdown; use UUID::Tiny qw(:std); @@ -45,6 +47,7 @@ sub send_registration_mail { my $ua = $self->req->headers->user_agent; my $reg_url = $self->url_for('reg')->to_abs->scheme('https'); + my $tos_url = $self->url_for('tos')->to_abs->scheme('https'); my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https'); my $body = "Hallo, ${user}!\n\n"; @@ -53,7 +56,8 @@ sub send_registration_mail { $body .= "Falls die Registrierung von dir ausging, kannst du den Account unter\n"; $body .= "${reg_url}/${user_id}/${token}\n"; - $body .= "freischalten.\n\n"; + $body .= "freischalten.\n"; + $body .= "Beachte dabei die Nutzungsbedingungen: ${tos_url}\n\n"; $body .= "Falls nicht, ignoriere diese Mail bitte. Nach etwa 48 Stunden wird deine\n"; $body @@ -831,29 +835,6 @@ sub insight { } -sub services { - my ($self) = @_; - my $user = $self->current_user; - - if ( $self->param('action') and $self->param('action') eq 'save' ) { - my $sb = $self->param('stationboard'); - my $value = 0; - if ( $sb =~ m{ ^ \d+ $ }x and $sb >= 0 and $sb <= 4 ) { - $value = int($sb); - } - $self->users->use_external_services( - uid => $user->{id}, - set => $value - ); - $self->flash( success => 'external' ); - $self->redirect_to('account'); - } - - $self->param( stationboard => - $self->users->use_external_services( uid => $user->{id} ) ); - $self->render('use_external_links'); -} - sub webhook { my ($self) = @_; @@ -1022,6 +1003,273 @@ sub password_form { $self->render('change_password'); } +sub lonlat_in_polygon { + my ( $self, $polygon, $lonlat ) = @_; + + my $circle = shift( @{$polygon} ); + my @holes = @{$polygon}; + + my $circle_poly = Math::Polygon->new( @{$circle} ); + if ( $circle_poly->contains($lonlat) ) { + for my $hole (@holes) { + my $hole_poly = Math::Polygon->new( @{$hole} ); + if ( $hole_poly->contains($lonlat) ) { + return; + } + } + return 1; + } + return; +} + +sub backend_form { + my ($self) = @_; + my $user = $self->current_user; + + my @backends = $self->stations->get_backends; + my @suggested_backends; + + my %place_map = ( + AT => 'Österreich', + CH => 'Schweiz', + 'CH-BE' => 'Kanton Bern', + 'CH-GE' => 'Kanton Genf', + 'CH-LU' => 'Kanton Luzern', + 'CH-ZH' => 'Kanton Zürich', + DE => 'Deutschland', + 'DE-BB' => 'Brandenburg', + 'DE-BW' => 'Baden-Württemberg', + 'DE-BE' => 'Berlin', + 'DE-BY' => 'Bayern', + 'DE-HB' => 'Bremen', + 'DE-HE' => 'Hessen', + 'DE-MV' => 'Mecklenburg-Vorpommern', + 'DE-NI' => 'Niedersachsen', + 'DE-NW' => 'Nordrhein-Westfalen', + 'DE-RP' => 'Rheinland-Pfalz', + 'DE-SH' => 'Schleswig-Holstein', + 'DE-ST' => 'Sachsen-Anhalt', + 'DE-TH' => 'Thüringen', + DK => 'Dänemark', + 'GB-NIR' => 'Nordirland', + LI => 'Liechtenstein', + LU => 'Luxembourg', + IE => 'Irland', + 'US-CA' => 'California', + 'US-TX' => 'Texas', + ); + + my ( $user_lat, $user_lon ) + = $self->journeys->get_latest_checkout_latlon( uid => $user->{id} ); + + for my $backend (@backends) { + my $type = 'UNKNOWN'; + if ( $backend->{iris} ) { + $type = 'IRIS-TTS'; + $backend->{name} = 'IRIS'; + $backend->{longname} = 'Deutsche Bahn: IRIS-TTS'; + $backend->{homepage} = 'https://www.bahn.de'; + $backend->{legacy} = 1; + } + elsif ( $backend->{dbris} ) { + $type = 'DBRIS'; + $backend->{longname} = 'Deutsche Bahn: bahn.de'; + $backend->{homepage} = 'https://www.bahn.de'; + $backend->{recommended} = 1; + } + elsif ( $backend->{efa} ) { + if ( my $s = $self->efa->get_service( $backend->{name} ) ) { + $type = 'EFA'; + $backend->{longname} = $s->{name}; + $backend->{homepage} = $s->{homepage}; + $backend->{regions} = [ map { $place_map{$_} // $_ } + @{ $s->{coverage}{regions} // [] } ]; + $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + $backend->{association} = 1; + + if ( + $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'Polygon' + and $self->lonlat_in_polygon( + $s->{coverage}{area}{coordinates}, + [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + } + elsif ( $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'MultiPolygon' ) + { + for my $s_poly ( + @{ $s->{coverage}{area}{coordinates} // [] } ) + { + if ( + $self->lonlat_in_polygon( + $s_poly, [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + last; + } + } + } + } + else { + $type = undef; + } + } + elsif ( $backend->{hafas} ) { + + # These backends lack a journey endpoint or are no longer + # operational and are thus useless for travelynx + if ( $backend->{name} eq 'Resrobot' + or $backend->{name} eq 'TPG' + or $backend->{name} eq 'VRN' + or $backend->{name} eq 'DB' ) + { + $type = undef; + } + + # PKP is behind a GeoIP filter. Only list it if travelynx.conf + # indicates that our IP is allowed or provides a proxy. + elsif ( + $backend->{name} eq 'PKP' + and not( $self->app->config->{hafas}{PKP}{geoip_ok} + or $self->app->config->{hafas}{PKP}{proxy} ) + ) + { + $type = undef; + } + elsif ( my $s = $self->hafas->get_service( $backend->{name} ) ) { + $type = 'HAFAS'; + $backend->{longname} = $s->{name}; + $backend->{homepage} = $s->{homepage}; + $backend->{regions} = [ map { $place_map{$_} // $_ } + @{ $s->{coverage}{regions} // [] } ]; + $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + + if ( $backend->{name} eq 'ÖBB' ) { + $backend->{recommended} = 1; + } + else { + $backend->{association} = 1; + } + + if ( + $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'Polygon' + and $self->lonlat_in_polygon( + $s->{coverage}{area}{coordinates}, + [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + } + elsif ( $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'MultiPolygon' ) + { + for my $s_poly ( + @{ $s->{coverage}{area}{coordinates} // [] } ) + { + if ( + $self->lonlat_in_polygon( + $s_poly, [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + last; + } + } + } + } + else { + $type = undef; + } + } + elsif ( $backend->{motis} ) { + my $s = $self->motis->get_service( $backend->{name} ); + + $type = 'MOTIS'; + $backend->{longname} = $s->{name}; + $backend->{homepage} = $s->{homepage}; + $backend->{regions} = [ map { $place_map{$_} // $_ } + @{ $s->{coverage}{regions} // [] } ]; + $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + $backend->{experimental} = 1; + + if ( $backend->{name} eq 'transitous' ) { + $backend->{regions} = ['Weltweit']; + } + if ( $backend->{name} eq 'RNV' ) { + $backend->{homepage} = 'https://rnv-online.de/'; + } + + if ( + $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'Polygon' + and $self->lonlat_in_polygon( + $s->{coverage}{area}{coordinates}, + [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + } + elsif ( $s->{coverage}{area} + and $s->{coverage}{area}{type} eq 'MultiPolygon' ) + { + for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } ) + { + if ( + $self->lonlat_in_polygon( + $s_poly, [ $user_lon, $user_lat ] + ) + ) + { + push( @suggested_backends, $backend ); + last; + } + } + } + } + $backend->{type} = $type; + } + + @backends = map { $_->[1] } + sort { $a->[0] cmp $b->[0] } + map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends; + + $self->render( + 'select_backend', + suggestions => \@suggested_backends, + backends => \@backends, + user => $user, + redirect_to => $self->req->param('redirect_to') // '/', + ); +} + +sub change_backend { + my ($self) = @_; + + my $backend_id = $self->req->param('backend'); + my $redir = $self->req->param('redirect_to') // '/'; + + if ( $backend_id !~ m{ ^ \d+ $ }x ) { + $self->redirect_to($redir); + } + + $self->users->set_backend( + uid => $self->current_user->{id}, + backend_id => $backend_id, + ); + + $self->redirect_to($redir); +} + sub change_password { my ($self) = @_; my $old_password = $self->req->param('oldpw'); diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm index 687243d..572d3fa 100755 --- a/lib/Travelynx/Controller/Api.pm +++ b/lib/Travelynx/Controller/Api.pm @@ -21,6 +21,9 @@ sub sanitize { if ( not defined $value ) { return undef; } + if ( not defined $type ) { + return $value ? ( '' . $value ) : undef; + } if ( $type eq '' ) { return '' . $value; } @@ -51,6 +54,8 @@ sub documentation { sub get_v1 { my ($self) = @_; + $self->res->headers->access_control_allow_origin(q{*}); + my $api_action = $self->stash('user_action'); my $api_token = $self->stash('token'); if ( $api_action !~ qr{ ^ (?: status | history | action ) $ }x ) { @@ -117,6 +122,7 @@ sub travel_v1 { deprecated => \0, error => 'Malformed JSON', }, + status => 400, ); return; } @@ -130,6 +136,7 @@ sub travel_v1 { deprecated => \0, error => 'Malformed token', }, + status => 400, ); return; } @@ -143,6 +150,7 @@ sub travel_v1 { deprecated => \0, error => 'Malformed token', }, + status => 400, ); return; } @@ -155,6 +163,7 @@ sub travel_v1 { deprecated => \0, error => 'Invalid token', }, + status => 400, ); return; } @@ -169,6 +178,7 @@ sub travel_v1 { error => 'Missing or invalid action', status => $self->get_user_status_json_v1( uid => $uid ) }, + status => 400, ); return; } @@ -177,7 +187,13 @@ sub travel_v1 { my $from_station = sanitize( q{}, $payload->{fromStation} ); my $to_station = sanitize( q{}, $payload->{toStation} ); my $train_id; - my $hafas = exists $payload->{train}{journeyID} ? 1 : 0; + my $dbris = sanitize( undef, $payload->{dbris} ); + my $hafas = sanitize( undef, $payload->{hafas} ); + my $motis = sanitize( undef, $payload->{motis} ); + + if ( not $hafas and exists $payload->{train}{journeyID} ) { + $dbris //= 'bahn.de'; + } if ( not( @@ -195,11 +211,15 @@ sub travel_v1 { error => 'Missing fromStation or train data', status => $self->get_user_status_json_v1( uid => $uid ) }, + status => 400, ); return; } - if ( not $hafas and not $self->stations->search($from_station) ) { + if ( not $hafas + and not $dbris + and not $self->stations->search( $from_station, backend_id => 1 ) ) + { $self->render( json => { success => \0, @@ -207,13 +227,15 @@ sub travel_v1 { error => 'Unknown fromStation', status => $self->get_user_status_json_v1( uid => $uid ) }, + status => 400, ); return; } if ( $to_station and not $hafas - and not $self->stations->search($to_station) ) + and not $dbris + and not $self->stations->search( $to_station, backend_id => 1 ) ) { $self->render( json => { @@ -222,6 +244,7 @@ sub travel_v1 { error => 'Unknown toStation', status => $self->get_user_status_json_v1( uid => $uid ) }, + status => 400, ); return; } @@ -273,7 +296,10 @@ sub travel_v1 { return $self->checkin_p( station => $from_station, train_id => $train_id, - uid => $uid + uid => $uid, + hafas => $hafas, + dbris => $dbris, + motis => $motis, ); } )->then( @@ -518,8 +544,9 @@ sub import_v1 { $payload->{toStation}{realTime} // $payload->{toStation}{scheduledTime} ), - comment => sanitize( q{}, $payload->{comment} ), - lax => $payload->{lax} ? 1 : 0, + comment => sanitize( q{}, $payload->{comment} ), + lax => $payload->{lax} ? 1 : 0, + backend_id => 1, ); if ( $payload->{intermediateStops} @@ -558,14 +585,20 @@ sub import_v1 { my $journey; if ( not $error ) { - $journey = $self->journeys->get_single( - uid => $uid, - db => $db, - journey_id => $journey_id, - verbose => 1 - ); - $error - = $self->journeys->sanity_check( $journey, $payload->{lax} ? 1 : 0 ); + eval { + $journey = $self->journeys->get_single( + uid => $uid, + db => $db, + journey_id => $journey_id, + verbose => 1 + ); + $error + = $self->journeys->sanity_check( $journey, + $payload->{lax} ? 1 : 0 ); + }; + if ($@) { + $error = $@; + } } if ($error) { @@ -654,10 +687,14 @@ sub autocomplete { $self->res->headers->cache_control('max-age=86400, immutable'); + my $backend_id = $self->param('backend_id') // 1; + my $output = "document.addEventListener('DOMContentLoaded',function(){M.Autocomplete.init(document.querySelectorAll('.autocomplete'),{\n"; $output .= 'minLength:3,limit:50,data:'; - $output .= encode_json( $self->stations->get_for_autocomplete ); + $output + .= encode_json( + $self->stations->get_for_autocomplete( backend_id => $backend_id ) ); $output .= "\n});});\n"; $self->render( diff --git a/lib/Travelynx/Controller/Passengerrights.pm b/lib/Travelynx/Controller/Passengerrights.pm index d80f1ae..5759d2e 100644 --- a/lib/Travelynx/Controller/Passengerrights.pm +++ b/lib/Travelynx/Controller/Passengerrights.pm @@ -121,6 +121,8 @@ sub list_candidates { } } + my @abo_journeys + = grep { $_->{delay} >= 20 and $_->{delay} < 60 } @journeys; @journeys = grep { $_->{delay} >= 60 or $_->{connection_missed} } @journeys; my @cancelled = $self->journeys->get( @@ -154,8 +156,9 @@ sub list_candidates { $self->respond_to( json => { json => [@journeys] }, any => { - template => 'passengerrights', - journeys => [@journeys] + template => 'passengerrights', + journeys => [@journeys], + abo_journeys => [@abo_journeys] } ); } diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm index a063c10..db30d36 100755 --- a/lib/Travelynx/Controller/Profile.pm +++ b/lib/Travelynx/Controller/Profile.pm @@ -111,6 +111,13 @@ sub profile { $status->{arr_name} = undef; } + my $map_data = {}; + if ( $status->{checked_in} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } + my @journeys; if ( @@ -152,34 +159,47 @@ sub profile { @journeys = $self->journeys->get(%opt); } - $self->render( - 'profile', - title => "travelynx: $name", - name => $name, - uid => $user->{id}, - privacy => $user, - bio => $profile->{bio}{html}, - metadata => $profile->{metadata}, - is_self => $is_self, - following => ( $relation and $relation eq 'follows' ) ? 1 : 0, - follow_requested => ( $relation and $relation eq 'requests_follow' ) - ? 1 - : 0, - can_follow => ( $my_user and $user->{accept_follows} and not $relation ) - ? 1 - : 0, - can_request_follow => - ( $my_user and $user->{accept_follow_requests} and not $relation ) - ? 1 - : 0, - follows_me => ( $inverse_relation and $inverse_relation eq 'follows' ) - ? 1 - : 0, - follow_reqs_me => - ( $inverse_relation and $inverse_relation eq 'requests_follow' ) ? 1 - : 0, - journey => $status, - journeys => [@journeys], + $self->respond_to( + json => { + json => { + name => $name, + uid => $user->{id}, + bio => $profile->{bio}{html}, + metadata => $profile->{metadata}, + } + }, + any => { + template => 'profile', + title => "travelynx: $name", + name => $name, + uid => $user->{id}, + privacy => $user, + bio => $profile->{bio}{html}, + metadata => $profile->{metadata}, + is_self => $is_self, + following => ( $relation and $relation eq 'follows' ) ? 1 : 0, + follow_requested => ( $relation and $relation eq 'requests_follow' ) + ? 1 + : 0, + can_follow => + ( $my_user and $user->{accept_follows} and not $relation ) ? 1 + : 0, + can_request_follow => ( + $my_user and $user->{accept_follow_requests} and not $relation + ) ? 1 + : 0, + follows_me => + ( $inverse_relation and $inverse_relation eq 'follows' ) ? 1 + : 0, + follow_reqs_me => ( + $inverse_relation and $inverse_relation eq 'requests_follow' + ) ? 1 + : 0, + journey => $status, + journeys => [@journeys], + with_map => 1, + %{$map_data}, + } ); } @@ -220,12 +240,13 @@ sub journey_details { } my $journey = $self->journeys->get_single( - uid => $user->{id}, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, - with_polyline => 1, - with_visibility => 1, + uid => $user->{id}, + journey_id => $journey_id, + verbose => 1, + with_datetime => 1, + with_route_datetime => 1, + with_polyline => 1, + with_visibility => 1, ); if ( not $journey ) { @@ -334,7 +355,16 @@ sub user_status { my $user = $self->users->get_privacy_by( name => $name ); if ( not $user ) { - $self->render( 'not_found', status => 404 ); + $self->respond_to( + json => { + json => { error => 'not found' }, + status => 404, + }, + any => { + template => 'not_found', + status => 404 + } + ); return; } @@ -389,11 +419,29 @@ sub user_status { "/p/${name}/j/$journey->{id}?token=${token}-${ts}"); } else { - $self->render( 'not_found', status => 404 ); + $self->respond_to( + json => { + json => { error => 'not found' }, + status => 404, + }, + any => { + template => 'not_found', + status => 404 + } + ); } return; } - $self->render( 'not_found', status => 404 ); + $self->respond_to( + json => { + json => { error => 'not found' }, + status => 404, + }, + any => { + template => 'not_found', + status => 404 + } + ); return; } @@ -455,6 +503,13 @@ sub user_status { $og_data{description} = $tw_data{description} = q{}; } + my $map_data = {}; + if ( $status->{checked_in} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } + $self->respond_to( json => { json => { @@ -477,7 +532,9 @@ sub user_status { journey => $status, twitter => \%tw_data, opengraph => \%og_data, - version => $self->app->config->{version} // 'UNKNOWN', + with_map => 1, + %{$map_data}, + version => $self->app->config->{version} // 'UNKNOWN', }, ); } @@ -516,6 +573,7 @@ sub status_card { my $status = $self->get_user_status( $user->{id} ); my $visibility; + my $map_data = {}; if ( $status->{checked_in} or $status->{arr_name} ) { my $visibility = $status->{effective_visibility}; if ( @@ -540,12 +598,19 @@ sub status_card { $status->{arr_name} = undef; } + if ( $status->{checked_in} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } + $self->render( '_public_status_card', name => $name, privacy => $user, journey => $status, from_profile => $self->param('profile') ? 1 : 0, + %{$map_data}, ); } diff --git a/lib/Travelynx/Controller/Static.pm b/lib/Travelynx/Controller/Static.pm index 04c2d0f..bcd6fda 100644 --- a/lib/Travelynx/Controller/Static.pm +++ b/lib/Travelynx/Controller/Static.pm @@ -35,4 +35,10 @@ sub offline { $self->render('offline'); } +sub tos { + my ($self) = @_; + + $self->render('terms-of-service'); +} + 1; diff --git a/lib/Travelynx/Controller/Traewelling.pm b/lib/Travelynx/Controller/Traewelling.pm index 3cdeff8..6aa789c 100644 --- a/lib/Travelynx/Controller/Traewelling.pm +++ b/lib/Travelynx/Controller/Traewelling.pm @@ -29,7 +29,7 @@ sub oauth { redirect_uri => $self->base_url_for('/oauth/traewelling')->to_abs->scheme( $self->app->mode eq 'development' ? 'http' : 'https' - )->to_string, + )->to_string, scope => 'read-statuses write-statuses' } )->then( diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 89385e1..fd2abb1 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -1,6 +1,7 @@ package Travelynx::Controller::Traveling; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Controller'; @@ -10,6 +11,7 @@ use DateTime::Format::Strptime; use List::Util qw(uniq min max); use List::UtilsBy qw(max_by uniq_by); use List::MoreUtils qw(first_index); +use Mojo::UserAgent; use Mojo::Promise; use Text::CSV; use Travel::Status::DE::IRIS::Stations; @@ -24,10 +26,15 @@ sub has_str_in_list { return; } +# when called with "eva" provided: look up connections from eva, either +# for provided backend_id / hafas or (if not provided) for user backend id. +# When calld without "eva": look up connections from current/latest arrival +# eva, using the checkin's backend id. sub get_connecting_trains_p { my ( $self, %opt ) = @_; - my $uid = $opt{uid} //= $self->current_user->{id}; + my $user = $self->current_user; + my $uid = $opt{uid} //= $user->{id}; my $use_history = $self->users->use_history( uid => $uid ); my ( $eva, $exclude_via, $exclude_train_id, $exclude_before ); @@ -36,6 +43,23 @@ sub get_connecting_trains_p { my $promise = Mojo::Promise->new; + if ( $user->{backend_dbris} ) { + + # We do get a little bit of via information, so this might work in some + # cases. But not reliably. Probably best to leave it out entirely then. + return $promise->reject; + } + if ( $user->{backend_efa} ) { + + # TODO + return $promise->reject; + } + if ( $user->{backend_motis} ) { + + # FIXME: The following code can't handle external_ids currently + return $promise->reject; + } + if ( $opt{eva} ) { if ( $use_history & 0x01 ) { $eva = $opt{eva}; @@ -43,10 +67,20 @@ sub get_connecting_trains_p { elsif ( $opt{destination_name} ) { $eva = $opt{eva}; } + if ( not defined $opt{backend_id} ) { + if ( $opt{hafas} ) { + $opt{backend_id} + = $self->stations->get_backend_id( hafas => $opt{hafas} ); + } + else { + $opt{backend_id} = $user->{backend_id}; + } + } } else { if ( $use_history & 0x02 ) { my $status = $self->get_user_status; + $opt{backend_id} = $status->{backend_id}; $eva = $status->{arr_eva}; $exclude_via = $status->{dep_name}; $exclude_train_id = $status->{train_id}; @@ -65,10 +99,12 @@ sub get_connecting_trains_p { return $promise->reject; } - my ( $dest_ids, $destinations ) - = $self->journeys->get_connection_targets(%opt); + $self->log->debug( + "get_connecting_trains_p(backend_id => $opt{backend_id}, eva => $eva)"); - my @destinations = uniq_by { $_->{name} } @{$destinations}; + my @destinations = $self->journeys->get_connection_targets(%opt); + + @destinations = uniq_by { $_->{name} } @destinations; if ($exclude_via) { @destinations = grep { $_->{name} ne $exclude_via } @destinations; @@ -78,11 +114,8 @@ sub get_connecting_trains_p { return $promise->reject; } - my $iris_eva = $eva; - if ( $eva < 8000000 ) { - $iris_eva = ( List::Util::first { $_ >= 8000000 } @{$dest_ids} ) - // $eva; - } + $self->log->debug( 'get_connection_targets returned ' + . join( q{, }, map { $_->{name} } @destinations ) ); my $can_check_in = not $arr_epoch || ( $arr_countdown // 1 ) < 0; my $lookahead @@ -91,11 +124,11 @@ sub get_connecting_trains_p { my $iris_promise = Mojo::Promise->new; my %via_count = map { $_->{name} => 0 } @destinations; - if ( $iris_eva >= 8000000 - and List::Util::any { $_->{eva} >= 8000000 } @destinations ) - { + my $backend + = $self->stations->get_backend( backend_id => $opt{backend_id} ); + if ( $opt{backend_id} == 0 ) { $self->iris->get_departures_p( - station => $iris_eva, + station => $eva, lookbehind => 10, lookahead => $lookahead, with_related => 1 @@ -103,7 +136,7 @@ sub get_connecting_trains_p { sub { my ($stationboard) = @_; if ( $stationboard->{errstr} ) { - $iris_promise->resolve( [] ); + $promise->resolve( [], [] ); return; } @@ -237,105 +270,35 @@ sub get_connecting_trains_p { } } - $iris_promise->resolve( [ @results, @cancellations ] ); + $promise->resolve( [ @results, @cancellations ], [] ); return; } )->catch( sub { - $iris_promise->resolve( [] ); + $promise->resolve( [], [] ); return; } )->wait; } - else { - $iris_promise->resolve( [] ); + elsif ( $backend->{dbris} ) { + return $promise->reject; } - - my $hafas_promise = Mojo::Promise->new; - $self->hafas->get_departures_p( - eva => $eva, - lookbehind => 10, - lookahead => $lookahead - )->then( - sub { - my ($status) = @_; - $hafas_promise->resolve( [ $status->results ] ); - return; - } - )->catch( - sub { - # HAFAS data is optional. - # Errors are logged by get_json_p and can be silently ignored here. - $hafas_promise->resolve( [] ); - return; - } - )->wait; - - Mojo::Promise->all( $iris_promise, $hafas_promise )->then( - sub { - my ( $iris, $hafas ) = @_; - my @iris_trains = @{ $iris->[0] }; - my @all_hafas_trains = @{ $hafas->[0] }; - my @hafas_trains; - - # We've already got a list of connecting trains; this function - # only adds further information to them. We ignore errors, as - # partial data is better than no data. - eval { - for my $iris_train (@iris_trains) { - if ( $iris_train->[0]->departure_is_cancelled ) { - for my $hafas_train (@all_hafas_trains) { - if ( $hafas_train->number - and $hafas_train->number - == $iris_train->[0]->train_no ) - { - $hafas_train->{iris_seen} = 1; - next; - } - } - next; - } - for my $hafas_train (@all_hafas_trains) { - if ( $hafas_train->number - and $hafas_train->number - == $iris_train->[0]->train_no ) - { - $hafas_train->{iris_seen} = 1; - if ( $hafas_train->load - and $hafas_train->load->{SECOND} ) - { - $iris_train->[3] = $hafas_train->load; - } - for my $stop ( $hafas_train->route ) { - if ( $stop->loc->name - and $stop->loc->name eq - $iris_train->[1]->{name} - and $stop->arr ) - { - $iris_train->[2] = $stop->arr; - if ( $iris_train->[0]->departure_delay - and not $stop->arr_delay ) - { - $iris_train->[2] - ->add( minutes => $iris_train->[0] - ->departure_delay ); - } - last; - } - } - last; - } - } - } + elsif ( $backend->{efa} ) { + return $promise->reject; + } + elsif ( $backend->{hafas} ) { + my $hafas_service = $backend->{name}; + $self->hafas->get_departures_p( + service => $hafas_service, + eva => $eva, + lookbehind => 10, + lookahead => $lookahead + )->then( + sub { + my ($status) = @_; + my @hafas_trains; + my @all_hafas_trains = $status->results; for my $hafas_train (@all_hafas_trains) { - if ( $hafas_train->{iris_seen} ) { - next; - } - if ( $hafas_train->station_eva >= 8000000 ) { - - # better safe than sorry, for now - next; - } for my $stop ( $hafas_train->route ) { for my $dest (@destinations) { if ( $stop->loc->name @@ -353,30 +316,30 @@ sub get_connecting_trains_p { } if ( $departure->epoch >= $exclude_before ) { $via_count{ $dest->{name} }++; - push( @hafas_trains, - [ $hafas_train, $dest, $arrival ] ); + push( + @hafas_trains, + [ + $hafas_train, $dest, + $arrival, $hafas_service + ] + ); } } } } } - }; - if ($@) { - $self->app->log->error( - "get_connecting_trains_p($uid): IRIS/HAFAS merge failed: $@" - ); + $promise->resolve( [], \@hafas_trains ); + return; } - - $promise->resolve( \@iris_trains, \@hafas_trains ); - return; - } - )->catch( - sub { - my ($err) = @_; - $promise->reject($err); - return; - } - )->wait; + )->catch( + sub { + my ($err) = @_; + $self->log->debug("get_connection_trains: hafas: $err"); + $promise->resolve( [], [] ); + return; + } + )->wait; + } return $promise; } @@ -394,7 +357,8 @@ sub compute_effective_visibility { sub homepage { my ($self) = @_; if ( $self->is_user_authenticated ) { - my $uid = $self->current_user->{id}; + my $user = $self->current_user; + my $uid = $user->{id}; my $status = $self->get_user_status; my @timeline = $self->in_transit->get_timeline( uid => $uid, @@ -403,9 +367,15 @@ sub homepage { $self->stash( timeline => [@timeline] ); my @recent_targets; if ( $status->{checked_in} ) { + my $map_data = {}; + if ( $status->{arr_name} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } my $journey_visibility = $self->compute_effective_visibility( - $self->current_user->{default_visibility_str}, + $user->{default_visibility_str}, $status->{visibility_str} ); if ( defined $status->{arrival_countdown} and $status->{arrival_countdown} < ( 40 * 60 ) ) @@ -416,10 +386,13 @@ sub homepage { my ( $connections_iris, $connections_hafas ) = @_; $self->render( 'landingpage', + user => $user, user_status => $status, journey_visibility => $journey_visibility, connections_iris => $connections_iris, connections_hafas => $connections_hafas, + with_map => 1, + %{$map_data}, ); $self->users->mark_seen( uid => $uid ); } @@ -427,8 +400,11 @@ sub homepage { sub { $self->render( 'landingpage', + user => $user, user_status => $status, journey_visibility => $journey_visibility, + with_map => 1, + %{$map_data}, ); $self->users->mark_seen( uid => $uid ); } @@ -438,23 +414,28 @@ sub homepage { else { $self->render( 'landingpage', + user => $user, user_status => $status, journey_visibility => $journey_visibility, + with_map => 1, + %{$map_data}, ); $self->users->mark_seen( uid => $uid ); return; } } else { - @recent_targets = uniq_by { $_->{eva} } + @recent_targets = uniq_by { $_->{external_id_or_eva} } $self->journeys->get_latest_checkout_stations( uid => $uid ); } $self->render( 'landingpage', + user => $user, user_status => $status, recent_targets => \@recent_targets, with_autocomplete => 1, - with_geolocation => 1 + with_geolocation => 1, + backend_id => $user->{backend_id}, ); $self->users->mark_seen( uid => $uid ); } @@ -476,6 +457,12 @@ sub status_card { $self->stash( timeline => [@timeline] ); if ( $status->{checked_in} ) { + my $map_data = {}; + if ( $status->{arr_name} ) { + $map_data = $self->journeys_to_map_data( + journeys => [$status], + ); + } my $journey_visibility = $self->compute_effective_visibility( $self->current_user->{default_visibility_str}, @@ -493,6 +480,7 @@ sub status_card { journey_visibility => $journey_visibility, connections_iris => $connections_iris, connections_hafas => $connections_hafas, + %{$map_data}, ); } )->catch( @@ -501,6 +489,7 @@ sub status_card { '_checked_in', journey => $status, journey_visibility => $journey_visibility, + %{$map_data}, ); } )->wait; @@ -510,11 +499,13 @@ sub status_card { '_checked_in', journey => $status, journey_visibility => $journey_visibility, + %{$map_data}, ); } elsif ( $status->{cancellation} ) { $self->render_later; $self->get_connecting_trains_p( + backend_id => $status->{backend_id}, eva => $status->{cancellation}{dep_eva}, destination_name => $status->{cancellation}{arr_name} )->then( @@ -563,14 +554,225 @@ sub status_card { sub geolocation { my ($self) = @_; - my $lon = $self->param('lon'); - my $lat = $self->param('lat'); + my $lon = $self->param('lon'); + my $lat = $self->param('lat'); + my $backend_id = $self->param('backend') // 0; if ( not $lon or not $lat ) { - $self->render( json => { error => 'Invalid lon/lat received' } ); + $self->render( + json => { error => "Invalid lon/lat (${lon}/${lat}) received" } ); + return; + } + + if ( $backend_id !~ m{ ^ \d+ $ }x ) { + $self->render( + json => { error => "Invalid backend (${backend_id}) received" } ); + return; + } + + my ( $dbris_service, $efa_service, $hafas_service, $motis_service ); + my $backend = $self->stations->get_backend( backend_id => $backend_id ); + if ( $backend->{dbris} ) { + $dbris_service = $backend->{name}; + } + if ( $backend->{efa} ) { + $efa_service = $backend->{name}; + } + elsif ( $backend->{hafas} ) { + $hafas_service = $backend->{name}; + } + elsif ( $backend->{motis} ) { + $motis_service = $backend->{name}; + } + + if ($dbris_service) { + $self->render_later; + + Travel::Status::DE::DBRIS->new_p( + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + geoSearch => { + latitude => $lat, + longitude => $lon + } + )->then( + sub { + my ($dbris) = @_; + my @results = map { + { + name => $_->name, + eva => $_->eva, + distance => 0, + dbris => $dbris_service, + } + } $dbris->results; + if ( @results > 10 ) { + @results = @results[ 0 .. 9 ]; + } + $self->render( + json => { + candidates => [@results], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + return; + } + elsif ($efa_service) { + $self->render_later; + + Travel::Status::DE::EFA->new_p( + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + service => $efa_service, + coord => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($efa) = @_; + my @results = map { + { + name => $_->full_name, + eva => $_->id_code, + distance => 0, + efa => $efa_service, + } + } $efa->results; + if ( @results > 10 ) { + @results = @results[ 0 .. 9 ]; + } + $self->render( + json => { + candidates => [@results], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + return; + } + elsif ($hafas_service) { + $self->render_later; + + my $agent = $self->ua; + if ( my $proxy = $self->app->config->{hafas}{$hafas_service}{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + + Travel::Status::DE::HAFAS->new_p( + promise => 'Mojo::Promise', + user_agent => $agent, + service => $hafas_service, + geoSearch => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($hafas) = @_; + my @hafas = map { + { + name => $_->name, + eva => $_->eva, + distance => $_->distance_m / 1000, + hafas => $hafas_service + } + } $hafas->results; + if ( @hafas > 10 ) { + @hafas = @hafas[ 0 .. 9 ]; + } + $self->render( + json => { + candidates => [@hafas], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + + return; + } + elsif ($motis_service) { + $self->render_later; + + Travel::Status::MOTIS->new_p( + promise => 'Mojo::Promise', + user_agent => $self->ua, + time_zone => 'Europe/Berlin', + + service => $motis_service, + stops_by_coordinate => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($motis) = @_; + my @motis = map { + { + id => $_->id, + name => $_->name, + distance => 0, + motis => $motis_service, + } + } $motis->results; + + if ( @motis > 10 ) { + @motis = @motis[ 0 .. 9 ]; + } + + $self->render( + json => { + candidates => [@motis], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + return; } - $self->render_later; my @iris = map { { @@ -588,48 +790,12 @@ sub geolocation { if ( @iris > 5 ) { @iris = @iris[ 0 .. 4 ]; } - - Travel::Status::DE::HAFAS->new_p( - promise => 'Mojo::Promise', - user_agent => $self->ua, - geoSearch => { - lat => $lat, - lon => $lon - } - )->then( - sub { - my ($hafas) = @_; - my @hafas = map { - { - name => $_->name, - eva => $_->eva, - distance => $_->distance_m / 1000, - hafas => 1 - } - } $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, - } - ); + $self->render( + json => { + candidates => [@iris], } - )->wait; + ); + } sub travel_action { @@ -684,8 +850,14 @@ sub travel_action { $promise->then( sub { return $self->checkin_p( - station => $params->{station}, - train_id => $params->{train} + dbris => $params->{dbris}, + efa => $params->{efa}, + hafas => $params->{hafas}, + motis => $params->{motis}, + station => $params->{station}, + train_id => $params->{train}, + train_suffix => $params->{suffix}, + ts => $params->{ts}, ); } )->then( @@ -713,8 +885,14 @@ sub travel_action { my ( $still_checked_in, undef ) = @_; if ( my $destination = $params->{dest} ) { my $station_link = '/s/' . $destination; - if ( $status->{train_id} =~ m{[|]} ) { - $station_link .= '?hafas=1'; + if ( $status->{is_dbris} ) { + $station_link .= '?dbris=' . $status->{backend_name}; + } + elsif ( $status->{is_efa} ) { + $station_link .= '?efa=' . $status->{backend_name}; + } + elsif ( $status->{is_hafas} ) { + $station_link .= '?hafas=' . $status->{backend_name}; } $self->render( json => { @@ -749,8 +927,14 @@ sub travel_action { sub { my ( $still_checked_in, $error ) = @_; my $station_link = '/s/' . $params->{station}; - if ( $status->{train_id} =~ m{[|]} ) { - $station_link .= '?hafas=1'; + if ( $status->{is_dbris} ) { + $station_link .= '?dbris=' . $status->{backend_name}; + } + elsif ( $status->{is_efa} ) { + $station_link .= '?efa=' . $status->{backend_name}; + } + elsif ( $status->{is_hafas} ) { + $station_link .= '?hafas=' . $status->{backend_name}; } if ($error) { @@ -800,8 +984,32 @@ sub travel_action { else { my $redir = '/'; if ( $status->{checked_in} or $status->{cancelled} ) { - if ( $status->{train_id} =~ m{[|]} ) { - $redir = '/s/' . $status->{dep_eva} . '?hafas=1'; + if ( $status->{is_dbris} ) { + $redir + = '/s/' + . $status->{dep_eva} + . '?dbris=' + . $status->{backend_name}; + } + elsif ( $status->{is_efa} ) { + $redir + = '/s/' + . $status->{dep_eva} . '?efa=' + . $status->{backend_name}; + } + elsif ( $status->{is_hafas} ) { + $redir + = '/s/' + . $status->{dep_eva} + . '?hafas=' + . $status->{backend_name}; + } + elsif ( $status->{is_motis} ) { + $redir + = '/s/' + . $status->{dep_external_id} + . '?motis=' + . $status->{backend_name}; } else { $redir = '/s/' . $status->{dep_ds100}; @@ -818,8 +1026,13 @@ sub travel_action { elsif ( $params->{action} eq 'cancelled_from' ) { $self->render_later; $self->checkin_p( + dbris => $params->{dbris}, + efa => $params->{efa}, + hafas => $params->{hafas}, + motis => $params->{motis}, station => $params->{station}, - train_id => $params->{train} + train_id => $params->{train}, + ts => $params->{ts}, )->then( sub { $self->render( @@ -920,7 +1133,8 @@ sub station { my $train = $self->param('train'); my $trip_id = $self->param('trip_id'); my $timestamp = $self->param('timestamp'); - my $uid = $self->current_user->{id}; + my $user = $self->current_user; + my $uid = $user->{id}; my @timeline = $self->in_transit->get_timeline( uid => $uid, @@ -928,7 +1142,6 @@ sub station { ); my %checkin_by_train; for my $checkin (@timeline) { - say $checkin->{train_id}; push( @{ $checkin_by_train{ $checkin->{train_id} } }, $checkin ); } $self->stash( checkin_by_train => \%checkin_by_train ); @@ -945,16 +1158,105 @@ sub station { $timestamp = DateTime->now( time_zone => 'Europe/Berlin' ); } - my $use_hafas = $self->param('hafas'); + my ( $dbris_service, $efa_service, $hafas_service, $motis_service ); + + if ( $self->param('dbris') ) { + $dbris_service = $self->param('dbris'); + } + elsif ( $self->param('efa') ) { + $efa_service = $self->param('efa'); + } + elsif ( $self->param('hafas') ) { + $hafas_service = $self->param('hafas'); + } + elsif ( $self->param('motis') ) { + $motis_service = $self->param('motis'); + } + else { + if ( $user->{backend_dbris} ) { + $dbris_service = $user->{backend_name}; + } + elsif ( $user->{backend_efa} ) { + $efa_service = $user->{backend_name}; + } + elsif ( $user->{backend_hafas} ) { + $hafas_service = $user->{backend_name}; + } + elsif ( $user->{backend_motis} ) { + $motis_service = $user->{backend_name}; + } + } + my $promise; - if ($use_hafas) { + if ($dbris_service) { + if ( $station !~ m{ [@] L = \d+ }x ) { + $self->render_later; + $self->dbris->get_station_id_p($station)->then( + sub { + my ($dbris_station) = @_; + $self->redirect_to( '/s/' . $dbris_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + $self->redirect_to('/'); + } + )->wait; + return; + } + $promise = $self->dbris->get_departures_p( + station => $station, + timestamp => $timestamp, + lookbehind => 30, + ); + } + elsif ($efa_service) { + $promise = $self->efa->get_departures_p( + service => $efa_service, + name => $station, + timestamp => $timestamp, + lookbehind => 10, + lookahead => 50, + ); + } + elsif ($hafas_service) { $promise = $self->hafas->get_departures_p( + service => $hafas_service, eva => $station, timestamp => $timestamp, lookbehind => 30, lookahead => 30, ); } + elsif ($motis_service) { + if ( $station !~ m/.*_.*/ ) { + $self->render_later; + $self->motis->get_station_by_query_p( + service => $motis_service, + query => $station, + )->then( + sub { + my ($motis_station) = @_; + $self->redirect_to( '/s/' . $motis_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + say "$err"; + + $self->redirect_to('/'); + } + )->wait; + return; + } + $promise = $self->motis->get_departures_p( + service => $motis_service, + station_id => $station, + timestamp => $timestamp, + lookbehind => 30, + lookahead => 30, + ); + } else { $promise = $self->iris->get_departures_p( station => $station, @@ -966,28 +1268,39 @@ sub station { $promise->then( sub { my ($status) = @_; - my $api_link; my @results; my $now = $self->now->epoch; my $now_within_range = abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0; - if ($use_hafas) { + if ($dbris_service) { + + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->dep->epoch ] } $status->results; + + $status = { + station_eva => $station, + related_stations => [], + }; - my $iris_eva = List::Util::min grep { $_ >= 1000000 } - @{ $status->station->{evas} // [] }; - if ($iris_eva) { - $api_link = '/s/' . $iris_eva; + if ( $station =~ m{ [@] O = (?<name> [^@]+ ) [@] }x ) { + $status->{station_name} = $+{name}; } + } + elsif ($hafas_service) { @results = map { $_->[0] } sort { $b->[1] <=> $a->[1] } map { [ $_, $_->datetime->epoch ] } $status->results; - $self->stations->add_meta( - eva => $status->station->{eva}, - meta => $status->station->{evas} // [] - ); + if ( $status->station->{eva} ) { + $self->stations->add_meta( + eva => $status->station->{eva}, + meta => $status->station->{evas} // [], + hafas => $hafas_service, + ); + } $status = { station_eva => $status->station->{eva}, station_name => ( @@ -997,9 +1310,30 @@ sub station { related_stations => [], }; } - else { + elsif ($efa_service) { + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->datetime->epoch ] } $status->results; + $status = { + station_eva => $status->stop->id_num, + station_name => $status->stop->full_name, + related_stations => [], + }; + } + elsif ($motis_service) { + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->stopover->departure->epoch ] } + $status->results; - $api_link = '/s/' . $status->{station_eva} . '?hafas=1'; + $status = { + station_eva => $station, + station_name => + $status->{results}->[0]->stopover->stop->name, + related_stations => [], + }; + } + else { # You can't check into a train which terminates here @results = grep { $_->departure } @{ $status->{results} }; @@ -1029,10 +1363,10 @@ sub station { } my $connections_p; - if ( $trip_id and $use_hafas ) { + if ( $trip_id and ( $dbris_service or $hafas_service ) ) { @results = grep { $_->id eq $trip_id } @results; } - elsif ( $train and not $use_hafas ) { + elsif ( $train and not $hafas_service ) { @results = grep { $_->type . ' ' . $_->train_no eq $train } @results; } @@ -1044,12 +1378,17 @@ sub station { $connections_p = $self->get_connecting_trains_p( eva => $user_status->{cancellation}{dep_eva}, destination_name => - $user_status->{cancellation}{arr_name} + $user_status->{cancellation}{arr_name}, + efa => $efa_service, + hafas => $hafas_service, ); } else { $connections_p = $self->get_connecting_trains_p( - eva => $status->{station_eva} ); + eva => $status->{station_eva}, + efa => $efa_service, + hafas => $hafas_service + ); } } @@ -1059,18 +1398,21 @@ sub station { my ( $connections_iris, $connections_hafas ) = @_; $self->render( 'departures', + user => $user, + dbris => $dbris_service, + efa => $efa_service, + hafas => $hafas_service, + motis => $motis_service, eva => $status->{station_eva}, datetime => $timestamp, now_in_range => $now_within_range, results => \@results, - hafas => $use_hafas, station => $status->{station_name}, related_stations => $status->{related_stations}, user_status => $user_status, can_check_out => $can_check_out, connections_iris => $connections_iris, connections_hafas => $connections_hafas, - api_link => $api_link, title => "travelynx: $status->{station_name}", ); } @@ -1078,16 +1420,19 @@ sub station { sub { $self->render( 'departures', + user => $user, + dbris => $dbris_service, + efa => $efa_service, + hafas => $hafas_service, + motis => $motis_service, eva => $status->{station_eva}, datetime => $timestamp, now_in_range => $now_within_range, results => \@results, - hafas => $use_hafas, station => $status->{station_name}, related_stations => $status->{related_stations}, user_status => $user_status, can_check_out => $can_check_out, - api_link => $api_link, title => "travelynx: $status->{station_name}", ); } @@ -1096,16 +1441,19 @@ sub station { else { $self->render( 'departures', + user => $user, + dbris => $dbris_service, + efa => $efa_service, + hafas => $hafas_service, + motis => $motis_service, eva => $status->{station_eva}, datetime => $timestamp, now_in_range => $now_within_range, results => \@results, - hafas => $use_hafas, station => $status->{station_name}, related_stations => $status->{related_stations}, user_status => $user_status, can_check_out => $can_check_out, - api_link => $api_link, title => "travelynx: $status->{station_name}", ); } @@ -1120,15 +1468,35 @@ sub station { status => 300, ); } - elsif ( $use_hafas and $status and $status->errcode eq 'LOCATION' ) + elsif ( $efa_service + and $status + and scalar $status->name_candidates ) + { + $self->render( + 'disambiguation', + suggestions => [ + map { { name => $_->name, eva => $_->id_num } } + $status->name_candidates + ], + status => 300, + ); + } + elsif ( $hafas_service + and $status + and $status->errcode eq 'LOCATION' ) { - $self->hafas->search_location_p( query => $station )->then( + $self->hafas->search_location_p( + service => $hafas_service, + query => $station + )->then( sub { my ($hafas2) = @_; my @suggestions = $hafas2->results; if ( @suggestions == 1 ) { - $self->redirect_to( - '/s/' . $suggestions[0]->eva . '?hafas=1' ); + $self->redirect_to( '/s/' + . $suggestions[0]->eva + . '?hafas=' + . $hafas_service ); } else { $self->render( @@ -1153,11 +1521,30 @@ sub station { } )->wait; } + elsif ( $err + =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden|HTTP 500 Internal Server Error} + ) + { + $self->render( + 'bad_gateway', + message => $err, + status => 502, + select_new_backend => 1, + ); + } + elsif ( $err =~ m{timeout}i ) { + $self->render( + 'gateway_timeout', + message => $err, + status => 504, + select_new_backend => 1, + ); + } else { $self->render( 'exception', exception => $err, - status => 502 + status => 500 ); } } @@ -1169,25 +1556,49 @@ sub redirect_to_station { my ($self) = @_; my $station = $self->param('station'); - if ( my $s = $self->app->stations->search($station) ) { - if ( $s->{source} == 1 ) { - $self->redirect_to("/s/${station}?hafas=1"); - } - else { - $self->redirect_to("/s/${station}"); - } + if ( $self->param('backend_dbris') ) { + $self->render_later; + $self->dbris->get_station_id_p($station)->then( + sub { + my ($dbris_station) = @_; + $self->redirect_to( '/s/' . $dbris_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + $self->redirect_to('/'); + } + )->wait; + } + elsif ( $self->param('backend_motis') ) { + $self->render_later; + $self->motis->get_station_by_query( + service => $self->param('backend_motis'), + query => $station, + )->then( + sub { + my ($motis_station) = @_; + $self->redirect_to( '/s/' . $motis_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + $self->redirect_to('/'); + } + )->wait; } else { - $self->redirect_to("/s/${station}?hafas=1"); + $self->redirect_to("/s/${station}"); } } sub cancelled { my ($self) = @_; my @journeys = $self->journeys->get( - uid => $self->current_user->{id}, - cancelled => 1, - with_datetime => 1 + uid => $self->current_user->{id}, + cancelled => 1, + with_datetime => 1, + with_route_datetime => 1 ); $self->respond_to( @@ -1323,8 +1734,6 @@ sub commute { sub map_history { my ($self) = @_; - my $location = $self->app->coordinates_by_station; - if ( not $self->param('route_type') ) { $self->param( route_type => 'polybee' ); } @@ -1430,15 +1839,19 @@ sub csv_history { my $buf = q{}; $csv->combine( - qw(Zugtyp Linie Nummer Start Ziel), - 'Start (DS100)', - 'Ziel (DS100)', - 'Abfahrt (soll)', - 'Abfahrt (ist)', - 'Ankunft (soll)', - 'Ankunft (ist)', - 'Kommentar', - 'ID' + qw(type line number), + 'departure stop name', + 'departure stop id', + 'arrival stop name', + 'arrival stop id', + 'scheduled departure', + 'real-time departure', + 'scheduled arrival', + 'real-time arrival', + 'operator', + 'carriage type', + 'comment', + 'id' ); $buf .= $csv->string; @@ -1455,13 +1868,17 @@ sub csv_history { $journey->{line}, $journey->{no}, $journey->{from_name}, + $journey->{from_eva}, $journey->{to_name}, - $journey->{from_ds100}, - $journey->{to_ds100}, - $journey->{sched_departure}->strftime('%Y-%m-%d %H:%M'), - $journey->{rt_departure}->strftime('%Y-%m-%d %H:%M'), - $journey->{sched_arrival}->strftime('%Y-%m-%d %H:%M'), - $journey->{rt_arrival}->strftime('%Y-%m-%d %H:%M'), + $journey->{to_eva}, + $journey->{sched_departure}->strftime('%Y-%m-%d %H:%M:%S'), + $journey->{rt_departure}->strftime('%Y-%m-%d %H:%M:%S'), + $journey->{sched_arrival}->strftime('%Y-%m-%d %H:%M:%S'), + $journey->{rt_arrival}->strftime('%Y-%m-%d %H:%M:%S'), + $journey->{user_data}{operator} // q{}, + join( q{ + }, + map { $_->{desc} // $_->{name} } + @{ $journey->{user_data}{wagongroups} // [] } ), $journey->{user_data}{comment} // q{}, $journey->{id} ) @@ -1510,7 +1927,7 @@ sub year_in_review { if ( not @journeys ) { $self->render( 'not_found', - message => 'Keine Zugfahrten im angefragten Jahr gefunden.', + message => 'Keine Fahrten im angefragten Jahr gefunden.', status => 404 ); return; @@ -1583,7 +2000,7 @@ sub yearly_history { $self->render( 'not_found', status => 404, - message => 'Keine Zugfahrten im angefragten Jahr gefunden.' + message => 'Keine Fahrten im angefragten Jahr gefunden.' ); return; } @@ -1660,7 +2077,7 @@ sub monthly_history { if ( not @journeys ) { $self->render( 'not_found', - message => 'Keine Zugfahrten im angefragten Monat gefunden.', + message => 'Keine Fahrten im angefragten Monat gefunden.', status => 404 ); return; @@ -1682,13 +2099,15 @@ sub monthly_history { } }, any => { - template => 'history_by_month', - title => "travelynx: $month_name $year", - journeys => [@journeys], - year => $year, - month => $month, - month_name => $month_name, - statistics => $stats + template => 'history_by_month', + title => "travelynx: $month_name $year", + journeys => [@journeys], + year => $year, + month => $month, + month_name => $month_name, + filter_from => $interval_start, + filter_to => $interval_end->clone->subtract( days => 1 ), + statistics => $stats } ); @@ -1714,12 +2133,13 @@ sub journey_details { } my $journey = $self->journeys->get_single( - uid => $uid, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, - with_polyline => 1, - with_visibility => 1, + uid => $uid, + journey_id => $journey_id, + verbose => 1, + with_datetime => 1, + with_route_datetime => 1, + with_polyline => 1, + with_visibility => 1, ); if ($journey) { @@ -1942,10 +2362,11 @@ sub edit_journey { } my $journey = $self->journeys->get_single( - uid => $uid, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, + uid => $uid, + journey_id => $journey_id, + verbose => 1, + with_datetime => 1, + with_route_datetime => 1, ); if ( not $journey ) { @@ -2046,11 +2467,12 @@ sub edit_journey { if ( not $error ) { $journey = $self->journeys->get_single( - uid => $uid, - db => $db, - journey_id => $journey_id, - verbose => 1, - with_datetime => 1, + uid => $uid, + db => $db, + journey_id => $journey_id, + verbose => 1, + with_datetime => 1, + with_route_datetime => 1, ); $error = $self->journeys->sanity_check($journey); } @@ -2092,6 +2514,8 @@ sub edit_journey { sub add_journey_form { my ($self) = @_; + $self->stash( backend_id => $self->current_user->{backend_id} ); + if ( $self->param('action') and $self->param('action') eq 'save' ) { my $parser = DateTime::Format::Strptime->new( pattern => '%d.%m.%Y %H:%M', @@ -2112,8 +2536,9 @@ sub add_journey_form { $self->render( 'add_journey', with_autocomplete => 1, + status => 400, error => -'Zug muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.' +'Fahrt muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.' ); return; } @@ -2126,6 +2551,7 @@ sub add_journey_form { $self->render( 'add_journey', with_autocomplete => 1, + status => 400, error => "${key}: Ungültiges Datums-/Zeitformat" ); return; @@ -2148,8 +2574,9 @@ sub add_journey_form { my $db = $self->pg->db; my $tx = $db->begin; - $opt{db} = $db; - $opt{uid} = $self->current_user->{id}; + $opt{db} = $db; + $opt{uid} = $self->current_user->{id}; + $opt{backend_id} = $self->current_user->{backend_id}; my ( $journey_id, $error ) = $self->journeys->add(%opt); @@ -2167,6 +2594,7 @@ sub add_journey_form { $self->render( 'add_journey', with_autocomplete => 1, + status => 400, error => $error, ); } @@ -2184,4 +2612,241 @@ sub add_journey_form { } } +sub add_intransit_form { + my ($self) = @_; + + $self->stash( backend_id => $self->current_user->{backend_id} ); + + if ( $self->param('action') and $self->param('action') eq 'save' ) { + my $parser = DateTime::Format::Strptime->new( + pattern => '%d.%m.%Y %H:%M', + locale => 'de_DE', + time_zone => 'Europe/Berlin' + ); + my %opt; + my %trip; + + my @parts = split( qr{\s+}, $self->param('train') ); + + if ( @parts == 2 ) { + @trip{ 'train_type', 'train_no' } = @parts; + } + elsif ( @parts == 3 ) { + @trip{ 'train_type', 'train_line', 'train_no' } = @parts; + } + else { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => +'Fahrt muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.' + ); + return; + } + + for my $key (qw(sched_departure sched_arrival)) { + if ( $self->param($key) ) { + my $datetime = $parser->parse_datetime( $self->param($key) ); + if ( not $datetime ) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => "${key}: Ungültiges Datums-/Zeitformat" + ); + return; + } + $trip{$key} = $datetime; + } + } + + for my $key (qw(dep_station arr_station route comment)) { + $trip{$key} = $self->param($key); + } + + $opt{backend_id} = $self->current_user->{backend_id}; + + my $dep_stop = $self->stations->search( $trip{dep_station}, + backend_id => $opt{backend_id} ); + my $arr_stop = $self->stations->search( $trip{arr_station}, + backend_id => $opt{backend_id} ); + + if ( defined $trip{route} ) { + $trip{route} = [ split( qr{\r?\n\r?}, $trip{route} ) ]; + } + + my $route_has_start = 0; + my $route_has_stop = 0; + + for my $station ( @{ $trip{route} || [] } ) { + if ( $station eq $dep_stop->{name} + or $station eq $dep_stop->{eva} ) + { + $route_has_start = 1; + } + if ( $station eq $arr_stop->{name} + or $station eq $arr_stop->{eva} ) + { + $route_has_stop = 1; + } + } + + my @route; + + if ( not $route_has_start ) { + push( + @route, + [ + $dep_stop->{name}, + $dep_stop->{eva}, + { + lat => $dep_stop->{lat}, + lon => $dep_stop->{lon}, + } + ] + ); + } + + if ( $trip{route} ) { + my @unknown_stations; + my $prev_epoch; + for my $station ( @{ $trip{route} } ) { + my $ts; + my %station_data; + if ( $station + =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x + ) + { + $station = $+{stop}; + $ts = $parser->parse_datetime( $+{timestamp} ); + if ( $ts and $ts->epoch > $prev_epoch ) { + $station_data{sched_arr} = $ts->epoch; + $station_data{sched_dep} = $ts->epoch; + $prev_epoch = $ts->epoch; + } + else { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => "Ungültige Zeitangabe: $+{timestamp}" + ); + return; + } + } + my $station_info = $self->stations->search( $station, + backend_id => $opt{backend_id} ); + if ($station_info) { + $station_data{lat} = $station_info->{lat}; + $station_data{lon} = $station_info->{lon}; + push( + @route, + [ + $station_info->{name}, $station_info->{eva}, + \%station_data, + ] + ); + } + else { + push( @route, [ $station, undef, {} ] ); + push( @unknown_stations, $station ); + } + } + + if ( @unknown_stations == 1 ) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => "Unbekannter Unterwegshalt: $unknown_stations[0]" + ); + return; + } + elsif (@unknown_stations) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => 'Unbekannte Unterwegshalte: ' + . join( ', ', @unknown_stations ) + ); + return; + } + } + + if ( not $route_has_stop ) { + push( + @route, + [ + $arr_stop->{name}, + $arr_stop->{eva}, + { + lat => $arr_stop->{lat}, + lon => $arr_stop->{lon}, + } + ] + ); + } + + for my $station (@route) { + if ( $station->[0] eq $dep_stop->{name} + or $station->[1] eq $dep_stop->{eva} ) + { + $station->[2]{sched_dep} = $trip{sched_departure}->epoch; + } + if ( $station->[0] eq $arr_stop->{name} + or $station->[1] eq $arr_stop->{eva} ) + { + $station->[2]{sched_arr} = $trip{sched_arrival}->epoch; + } + } + + my $error; + my $db = $self->pg->db; + my $tx = $db->begin; + + $trip{dep_id} = $dep_stop->{eva}; + $trip{arr_id} = $arr_stop->{eva}; + $trip{route} = \@route; + + $opt{db} = $db; + $opt{manual} = \%trip; + $opt{uid} = $self->current_user->{id}; + + if ( not defined $trip{dep_id} ) { + $error = "Unknown departure stop '$trip{dep_station}'"; + } + elsif ( not defined $trip{arr_id} ) { + $error = "Unknown arrival stop '$trip{arr_station}'"; + } + elsif ( $trip{sched_arrival} <= $trip{sched_departure} ) { + $error = 'Ankunftszeit muss nach Abfahrtszeit liegen'; + } + else { + $error = $self->in_transit->add(%opt); + } + + if ($error) { + $self->render( + 'add_intransit', + with_autocomplete => 1, + status => 400, + error => $error, + ); + } + else { + $tx->commit; + $self->redirect_to('/'); + } + } + else { + $self->render( + 'add_intransit', + with_autocomplete => 1, + error => undef + ); + } +} + 1; |