summaryrefslogtreecommitdiff
path: root/lib/Travelynx/Controller
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx/Controller')
-rw-r--r--lib/Travelynx/Controller/Account.pm378
-rwxr-xr-xlib/Travelynx/Controller/Api.pm87
-rw-r--r--lib/Travelynx/Controller/Passengerrights.pm7
-rwxr-xr-xlib/Travelynx/Controller/Profile.pm145
-rw-r--r--lib/Travelynx/Controller/Static.pm14
-rw-r--r--lib/Travelynx/Controller/Traewelling.pm3
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm1704
7 files changed, 1887 insertions, 451 deletions
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm
index 2664329..533e6eb 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
@@ -799,11 +803,8 @@ sub profile {
sub insight {
my ($self) = @_;
- my $user = $self->current_user;
- my ( $use_history, $destinations ) = $self->users->use_history(
- uid => $user->{id},
- with_local_transit => 1
- );
+ my $user = $self->current_user;
+ my $use_history = $self->users->use_history( uid => $user->{id} );
if ( $self->param('action') and $self->param('action') eq 'save' ) {
if ( $self->param('on_departure') ) {
@@ -820,58 +821,20 @@ sub insight {
$use_history &= ~0x02;
}
- if ( $self->param('local_transit') ) {
- $use_history |= 0x04;
- }
- else {
- $use_history &= ~0x04;
- }
-
- if ( $self->param('destinations') ) {
- $destinations
- = [ split( qr{\r?\n\r?}, $self->param('destinations') ) ];
- }
-
$self->users->use_history(
- uid => $user->{id},
- set => $use_history,
- destinations => $destinations
+ uid => $user->{id},
+ set => $use_history
);
$self->flash( success => 'use_history' );
$self->redirect_to('account');
}
- $self->param( on_departure => $use_history & 0x01 ? 1 : 0 );
- $self->param( on_arrival => $use_history & 0x02 ? 1 : 0 );
- $self->param( local_transit => $use_history & 0x04 ? 1 : 0 );
- $self->param( destinations => join( "\n", @{$destinations} ) );
+ $self->param( on_departure => $use_history & 0x01 ? 1 : 0 );
+ $self->param( on_arrival => $use_history & 0x02 ? 1 : 0 );
$self->render('use_history');
}
-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) = @_;
@@ -911,6 +874,35 @@ sub webhook {
$self->render( 'webhooks', hook => $hook );
}
+sub change_language {
+ my ($self) = @_;
+
+ my $action = $self->req->param('action');
+ my $language = $self->req->param('language');
+
+ if ( $action and $action eq 'save' ) {
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
+ $self->users->set_language(
+ uid => $self->current_user->{id},
+ language => $language eq 'none' ? undef : $language,
+ );
+ $self->flash( success => 'language' );
+ $self->redirect_to('account');
+ }
+ else {
+ my @languages = @{ $self->current_user->{languages} };
+ $self->param( language => $languages[0] // 'none' );
+ $self->render('language');
+ }
+}
+
sub change_mail {
my ($self) = @_;
@@ -1040,6 +1032,287 @@ 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 %backend_by_id;
+ 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 defined $user_lon
+ 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'
+ and defined $user_lon )
+ {
+ 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 defined $user_lon
+ 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'
+ and defined $user_lon )
+ {
+ 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 defined $user_lon
+ 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'
+ and defined $user_lon )
+ {
+ 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;
+
+ $backend_by_id{ $backend->{id} } = $backend;
+ }
+
+ my @frequent_backends = grep { $_->{type} }
+ map { $backend_by_id{$_} }
+ $self->journeys->get_frequent_backend_ids( uid => $user->{id} );
+
+ @backends = map { $_->[1] }
+ sort { $a->[0] cmp $b->[0] }
+ map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends;
+
+ $self->render(
+ 'select_backend',
+ suggestions => \@suggested_backends,
+ frequent => \@frequent_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');
@@ -1236,6 +1509,15 @@ sub confirm_mail {
my $id = $self->current_user->{id};
my $token = $self->stash('token');
+ # Some mail clients include the trailing ">" from the confirmation mail
+ # when opening/copying the confirmation link. A token will never contain
+ # this symbol, so remove it just in case.
+ $token =~ s{>}{};
+
+ # I did not yet find a mail client that also includes the trailing ",",
+ # but you never now...
+ $token =~ s{,}{};
+
if (
$self->users->change_mail_with_token(
uid => $id,
diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm
index 7f72a53..fa40e76 100755
--- a/lib/Travelynx/Controller/Api.pm
+++ b/lib/Travelynx/Controller/Api.pm
@@ -7,6 +7,7 @@ use Mojo::Base 'Mojolicious::Controller';
use DateTime;
use List::Util;
+use Mojo::JSON qw(encode_json);
use UUID::Tiny qw(:std);
# Internal Helpers
@@ -20,6 +21,9 @@ sub sanitize {
if ( not defined $value ) {
return undef;
}
+ if ( not defined $type ) {
+ return $value ? ( '' . $value ) : undef;
+ }
if ( $type eq '' ) {
return '' . $value;
}
@@ -50,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 ) {
@@ -116,6 +122,7 @@ sub travel_v1 {
deprecated => \0,
error => 'Malformed JSON',
},
+ status => 400,
);
return;
}
@@ -129,6 +136,7 @@ sub travel_v1 {
deprecated => \0,
error => 'Malformed token',
},
+ status => 400,
);
return;
}
@@ -142,6 +150,7 @@ sub travel_v1 {
deprecated => \0,
error => 'Malformed token',
},
+ status => 400,
);
return;
}
@@ -154,6 +163,7 @@ sub travel_v1 {
deprecated => \0,
error => 'Invalid token',
},
+ status => 400,
);
return;
}
@@ -168,6 +178,7 @@ sub travel_v1 {
error => 'Missing or invalid action',
status => $self->get_user_status_json_v1( uid => $uid )
},
+ status => 400,
);
return;
}
@@ -176,7 +187,16 @@ 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 $efa = sanitize( undef, $payload->{efa} );
+ my $hafas = sanitize( undef, $payload->{hafas} );
+ my $motis = sanitize( undef, $payload->{motis} );
+
+ if ( not( $efa or $hafas or $motis )
+ and exists $payload->{train}{journeyID} )
+ {
+ $dbris //= 'bahn.de';
+ }
if (
not(
@@ -194,11 +214,14 @@ 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( $dbris or $efa or $hafas or $motis )
+ and not $self->stations->search( $from_station, backend_id => 1 ) )
+ {
$self->render(
json => {
success => \0,
@@ -206,13 +229,14 @@ 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 or $efa or $hafas or $motis )
+ and not $self->stations->search( $to_station, backend_id => 1 ) )
{
$self->render(
json => {
@@ -221,6 +245,7 @@ sub travel_v1 {
error => 'Unknown toStation',
status => $self->get_user_status_json_v1( uid => $uid )
},
+ status => 400,
);
return;
}
@@ -272,7 +297,11 @@ sub travel_v1 {
return $self->checkin_p(
station => $from_station,
train_id => $train_id,
- uid => $uid
+ uid => $uid,
+ dbris => $dbris,
+ efa => $efa,
+ hafas => $hafas,
+ motis => $motis,
);
}
)->then(
@@ -517,8 +546,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}
@@ -557,14 +587,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) {
@@ -648,4 +684,25 @@ sub set_token {
$self->redirect_to('account');
}
+sub autocomplete {
+ my ($self) = @_;
+
+ $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( backend_id => $backend_id ) );
+ $output .= "\n});});\n";
+
+ $self->render(
+ format => 'js',
+ data => $output
+ );
+}
+
1;
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 03f3b0d..978e3f8 100755
--- a/lib/Travelynx/Controller/Profile.pm
+++ b/lib/Travelynx/Controller/Profile.pm
@@ -111,6 +111,14 @@ sub profile {
$status->{arr_name} = undef;
}
+ my $map_data = {};
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ with_now_markers => 1,
+ );
+ }
+
my @journeys;
if (
@@ -152,33 +160,47 @@ sub profile {
@journeys = $self->journeys->get(%opt);
}
- $self->render(
- 'profile',
- 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},
+ }
);
}
@@ -219,12 +241,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 ) {
@@ -313,6 +336,7 @@ sub journey_details {
}
$self->render(
'journey',
+ title => "travelynx: $title",
error => undef,
journey => $journey,
with_map => 1,
@@ -332,7 +356,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;
}
@@ -387,11 +420,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;
}
@@ -453,6 +504,14 @@ 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],
+ with_now_markers => 1,
+ );
+ }
+
$self->respond_to(
json => {
json => {
@@ -470,11 +529,14 @@ sub user_status {
any => {
template => 'user_status',
name => $name,
+ title => "travelynx: $tw_data{title}",
privacy => $user,
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',
},
);
}
@@ -513,6 +575,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 (
@@ -537,12 +600,20 @@ sub status_card {
$status->{arr_name} = undef;
}
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ with_now_markers => 1,
+ );
+ }
+
$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 c91630e..bcd6fda 100644
--- a/lib/Travelynx/Controller/Static.pm
+++ b/lib/Travelynx/Controller/Static.pm
@@ -8,25 +8,25 @@ use Mojo::Base 'Mojolicious::Controller';
sub about {
my ($self) = @_;
- $self->render('about');
+ $self->render( 'about', title => 'Über travelynx' );
}
sub changelog {
my ($self) = @_;
- $self->render('changelog');
+ $self->render( 'changelog', title => 'travelynx: Changelog' );
}
sub imprint {
my ($self) = @_;
- $self->render('imprint');
+ $self->render( 'imprint', title => 'travelynx: Impressum' );
}
sub legend {
my ($self) = @_;
- $self->render('legend');
+ $self->render( 'legend', title => 'travelynx: Legende' );
}
sub offline {
@@ -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 6c38f7a..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(
@@ -144,6 +144,7 @@ sub settings {
$self->param( tweet => 1 );
}
+ $self->stash( title => 'travelynx × träwelling' );
$self->render(
'traewelling',
traewelling => $traewelling,
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index 7ee04e0..0f31056 100755
--- a/lib/Travelynx/Controller/Traveling.pm
+++ b/lib/Travelynx/Controller/Traveling.pm
@@ -1,18 +1,22 @@
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';
use DateTime;
use DateTime::Format::Strptime;
+use GIS::Distance;
use List::Util qw(uniq min max);
use List::UtilsBy qw(max_by uniq_by);
-use List::MoreUtils qw(first_index);
+use List::MoreUtils qw(first_index last_index);
+use Mojo::UserAgent;
use Mojo::Promise;
use Text::CSV;
use Travel::Status::DE::IRIS::Stations;
+use XML::LibXML;
# Internal Helpers
@@ -24,14 +28,16 @@ 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 ( $use_history, $lt_stops ) = $self->users->use_history(
- uid => $uid,
- with_local_transit => 1
- );
+ 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 );
my $now = $self->now->epoch;
@@ -39,6 +45,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};
@@ -46,10 +69,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};
@@ -68,23 +101,34 @@ sub get_connecting_trains_p {
return $promise->reject;
}
+ $self->log->debug(
+ "get_connecting_trains_p(backend_id => $opt{backend_id}, eva => $eva)");
+
my @destinations = $self->journeys->get_connection_targets(%opt);
+ @destinations = uniq_by { $_->{name} } @destinations;
+
if ($exclude_via) {
- @destinations = grep { $_ ne $exclude_via } @destinations;
+ @destinations = grep { $_->{name} ne $exclude_via } @destinations;
}
- if ( not( @destinations or $use_history & 0x04 and @{$lt_stops} ) ) {
+ if ( not @destinations ) {
return $promise->reject;
}
+ $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
= $can_check_in ? 40 : ( ( ${arr_countdown} // 0 ) / 60 + 40 );
my $iris_promise = Mojo::Promise->new;
+ my %via_count = map { $_->{name} => 0 } @destinations;
- if (@destinations) {
+ my $backend
+ = $self->stations->get_backend( backend_id => $opt{backend_id} );
+ if ( $opt{backend_id} == 0 ) {
$self->iris->get_departures_p(
station => $eva,
lookbehind => 10,
@@ -94,7 +138,7 @@ sub get_connecting_trains_p {
sub {
my ($stationboard) = @_;
if ( $stationboard->{errstr} ) {
- $iris_promise->reject( $stationboard->{errstr} );
+ $promise->resolve( [], [] );
return;
}
@@ -105,7 +149,6 @@ sub get_connecting_trains_p {
my @results;
my @cancellations;
my $excluded_train;
- my %via_count = map { $_ => 0 } @destinations;
for my $train ( @{ $stationboard->{results} } ) {
if ( not $train->departure ) {
next;
@@ -144,7 +187,7 @@ sub get_connecting_trains_p {
$train->sched_route_post, $train->sched_route_end
);
for my $dest (@destinations) {
- if ( has_str_in_list( $dest, @via ) ) {
+ if ( has_str_in_list( $dest->{name}, @via ) ) {
push( @cancellations, [ $train, $dest ] );
next;
}
@@ -153,8 +196,8 @@ sub get_connecting_trains_p {
else {
my @via = ( $train->route_post, $train->route_end );
for my $dest (@destinations) {
- if ( $via_count{$dest} < 2
- and has_str_in_list( $dest, @via ) )
+ if ( $via_count{ $dest->{name} } < 2
+ and has_str_in_list( $dest->{name}, @via ) )
{
push( @results, [ $train, $dest ] );
@@ -162,7 +205,7 @@ sub get_connecting_trains_p {
if ( not $train->departure
or $train->departure->epoch >= $now )
{
- $via_count{$dest}++;
+ $via_count{ $dest->{name} }++;
}
next;
}
@@ -229,142 +272,76 @@ sub get_connecting_trains_p {
}
}
- $iris_promise->resolve( [ @results, @cancellations ] );
+ $promise->resolve( [ @results, @cancellations ], [] );
return;
}
)->catch(
sub {
- $iris_promise->reject(@_);
+ $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 @hafas_trains = @{ $hafas->[0] };
- my @transit_fyi;
-
- # 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 ) {
- next;
- }
- for my $hafas_train (@hafas_trains) {
- if ( $hafas_train->number
- and $hafas_train->number
- == $iris_train->[0]->train_no )
- {
- if ( $hafas_train->load
- and $hafas_train->load->{SECOND} )
+ 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) {
+ for my $stop ( $hafas_train->route ) {
+ for my $dest (@destinations) {
+ if ( $stop->loc->name
+ and $stop->loc->name eq $dest->{name}
+ and $via_count{ $dest->{name} } < 2
+ and $hafas_train->datetime )
{
- $iris_train->[3] = $hafas_train->load;
- }
- for my $stop ( $hafas_train->route ) {
- if ( $stop->{name}
- and $stop->{name} eq $iris_train->[1]
- and $stop->{arr} )
+ my $departure = $hafas_train->datetime;
+ my $arrival = $stop->arr;
+ my $delay = $hafas_train->delay;
+ if ( $delay
+ and $stop->arr == $stop->sched_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;
+ $arrival->add( minutes => $delay );
}
- }
- last;
- }
- }
- }
- if ( $use_history & 0x04 and @{$lt_stops} ) {
- my %via_count = map { $_ => 0 } @{$lt_stops};
- for my $hafas_train (@hafas_trains) {
- for my $stop ( $hafas_train->route ) {
- for my $dest ( @{$lt_stops} ) {
- if ( $stop->{name}
- and $stop->{name} eq $dest
- and $via_count{$dest} < 2
- and $hafas_train->datetime )
- {
- my $departure = $hafas_train->datetime;
- my $arrival = $stop->{arr};
- my $delay = $hafas_train->delay;
- if ( $delay
- and $stop->{arr} == $stop->{sched_arr} )
- {
- $arrival->add( minutes => $delay );
- }
- if ( $departure->epoch >= $exclude_before )
- {
- $via_count{$dest}++;
- push(
- @transit_fyi,
- [
- {
- line => $hafas_train->line,
- departure => $departure,
- departure_delay => $delay
- },
- $dest, $arrival
- ]
- );
- }
+ if ( $departure->epoch >= $exclude_before ) {
+ $via_count{ $dest->{name} }++;
+ 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, \@transit_fyi );
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
-
- # TODO logging. HAFAS errors should never happen, IRIS errors are noteworthy too.
- $promise->reject($err);
- return;
- }
- )->wait;
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->log->debug("get_connection_trains: hafas: $err");
+ $promise->resolve( [], [] );
+ return;
+ }
+ )->wait;
+ }
return $promise;
}
@@ -382,7 +359,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,
@@ -391,9 +369,17 @@ 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],
+ show_full_route => 1,
+ with_now_markers => 1,
+ );
+ }
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 ) )
@@ -401,13 +387,16 @@ sub homepage {
$self->render_later;
$self->get_connecting_trains_p->then(
sub {
- my ( $connecting_trains, $transit_fyi ) = @_;
+ my ( $connections_iris, $connections_hafas ) = @_;
$self->render(
'landingpage',
+ user => $user,
user_status => $status,
journey_visibility => $journey_visibility,
- connections => $connecting_trains,
- transit_fyi => $transit_fyi,
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
}
@@ -415,8 +404,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 );
}
@@ -426,23 +418,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 );
}
@@ -464,6 +461,14 @@ 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],
+ show_full_route => 1,
+ with_now_markers => 1,
+ );
+ }
my $journey_visibility
= $self->compute_effective_visibility(
$self->current_user->{default_visibility_str},
@@ -474,13 +479,14 @@ sub status_card {
$self->render_later;
$self->get_connecting_trains_p->then(
sub {
- my ( $connecting_trains, $transit_fyi ) = @_;
+ my ( $connections_iris, $connections_hafas ) = @_;
$self->render(
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
- connections => $connecting_trains,
- transit_fyi => $transit_fyi
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
+ %{$map_data},
);
}
)->catch(
@@ -489,6 +495,7 @@ sub status_card {
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
+ %{$map_data},
);
}
)->wait;
@@ -498,11 +505,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(
@@ -510,8 +519,8 @@ sub status_card {
my ($connecting_trains) = @_;
$self->render(
'_cancelled_departure',
- journey => $status->{cancellation},
- connections => $connecting_trains
+ journey => $status->{cancellation},
+ connections_iris => $connecting_trains
);
}
)->catch(
@@ -529,11 +538,12 @@ sub status_card {
$self->render_later;
$self->get_connecting_trains_p->then(
sub {
- my ($connecting_trains) = @_;
+ my ( $connections_iris, $connections_hafas ) = @_;
$self->render(
'_checked_out',
- journey => $status,
- connections => $connecting_trains
+ journey => $status,
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
);
}
)->catch(
@@ -550,14 +560,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,
+ }
+ } uniq_by { $_->name } $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 {
{
@@ -575,48 +796,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 {
@@ -671,8 +856,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(
@@ -700,8 +891,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 => {
@@ -736,8 +933,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) {
@@ -787,8 +990,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};
@@ -805,8 +1032,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(
@@ -902,10 +1134,13 @@ sub travel_action {
}
sub station {
- my ($self) = @_;
- my $station = $self->stash('station');
- my $train = $self->param('train');
- my $uid = $self->current_user->{id};
+ my ($self) = @_;
+ my $station = $self->stash('station');
+ my $train = $self->param('train');
+ my $trip_id = $self->param('trip_id');
+ my $timestamp = $self->param('timestamp');
+ my $user = $self->current_user;
+ my $uid = $user->{id};
my @timeline = $self->in_transit->get_timeline(
uid => $uid,
@@ -913,18 +1148,117 @@ 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 );
$self->render_later;
- my $use_hafas = $self->param('hafas');
+ if ( $timestamp and $timestamp =~ m{ ^ \d+ $ }x ) {
+ $timestamp = DateTime->from_epoch(
+ epoch => $timestamp,
+ time_zone => 'Europe/Berlin'
+ );
+ }
+ else {
+ $timestamp = DateTime->now( time_zone => 'Europe/Berlin' );
+ }
+
+ 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,
);
@@ -940,25 +1274,39 @@ sub station {
$promise->then(
sub {
my ($status) = @_;
- my $api_link;
my @results;
- if ($use_hafas) {
+ my $now = $self->now->epoch;
+ my $now_within_range
+ = abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0;
+
+ if ($dbris_service) {
- my $iris_eva = List::Util::min grep { $_ >= 1000000 }
- @{ $status->station->{evas} // [] };
- if ($iris_eva) {
- $api_link = '/s/' . $iris_eva;
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->dep->epoch ] } $status->results;
+
+ $status = {
+ station_eva => $station,
+ related_stations => [],
+ };
+
+ if ( $station =~ m{ [@] O = (?<name> [^@]+ ) [@] }x ) {
+ $status->{station_name} = $+{name};
}
+ }
+ elsif ($hafas_service) {
- my $now = $self->now->epoch;
@results = map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
- map { [ $_, $_->datetime->epoch ] }
- grep {
- ( $_->datetime // $_->sched_datetime )->epoch
- < $now + 30 * 60
- } $status->results;
+ map { [ $_, $_->datetime->epoch ] } $status->results;
+ 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 => (
@@ -968,9 +1316,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} };
@@ -1000,7 +1369,10 @@ sub station {
}
my $connections_p;
- if ($train) {
+ if ( $trip_id and ( $dbris_service or $hafas_service ) ) {
+ @results = grep { $_->id eq $trip_id } @results;
+ }
+ elsif ( $train and not $hafas_service ) {
@results
= grep { $_->type . ' ' . $_->train_no eq $train } @results;
}
@@ -1012,30 +1384,41 @@ 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
+ );
}
}
if ($connections_p) {
$connections_p->then(
sub {
- my ($connecting_trains) = @_;
+ my ( $connections_iris, $connections_hafas ) = @_;
$self->render(
'departures',
- eva => $status->{station_eva},
- 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 => $connecting_trains,
- api_link => $api_link,
+ 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,
+ 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,
title => "travelynx: $status->{station_name}",
);
}
@@ -1043,14 +1426,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}",
);
}
@@ -1059,14 +1447,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}",
);
}
@@ -1081,25 +1474,42 @@ 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' )
{
- $status->similar_stops_p->then(
+ $self->hafas->search_location_p(
+ service => $hafas_service,
+ query => $station
+ )->then(
sub {
- my @suggestions = @_;
+ my ($hafas2) = @_;
+ my @suggestions = $hafas2->results;
if ( @suggestions == 1 ) {
- $self->redirect_to(
- '/s/' . $suggestions[0]->{id} . '?hafas=1' );
+ $self->redirect_to( '/s/'
+ . $suggestions[0]->eva
+ . '?hafas='
+ . $hafas_service );
}
else {
$self->render(
'disambiguation',
suggestions => [
- map {
- {
- name => $_->{name},
- eva => $_->{id}
- }
- } @suggestions
+ map { { name => $_->name, eva => $_->eva } }
+ @suggestions
],
status => 300,
);
@@ -1111,17 +1521,36 @@ sub station {
$self->render(
'exception',
exception =>
- "StopFinder threw '$err2' when handling '$err'",
+"locationSearch threw '$err2' when handling '$err'",
status => 502
);
}
)->wait;
}
+ elsif ( $err
+ =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden|HTTP 500 Internal Server Error|HTTP 429 Too Many Requests}
+ )
+ {
+ $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
);
}
}
@@ -1133,25 +1562,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(
@@ -1166,7 +1619,10 @@ sub cancelled {
sub history {
my ($self) = @_;
- $self->render( template => 'history' );
+ $self->render(
+ template => 'history',
+ title => 'travelynx: History'
+ );
}
sub commute {
@@ -1274,6 +1730,7 @@ sub commute {
journeys_by_month => \%journeys_by_month,
count_by_month => \%count_by_month,
total_journeys => $total,
+ title => 'travelynx: Reisen nach Station',
months => [
qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
],
@@ -1283,42 +1740,52 @@ 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' );
}
my $route_type = $self->param('route_type');
- my $filter_from = $self->param('filter_after');
- my $filter_until = $self->param('filter_before');
+ my $filter_from = $self->param('filter_from');
+ my $filter_until = $self->param('filter_to');
my $filter_type = $self->param('filter_type');
my $with_polyline = $route_type eq 'beeline' ? 0 : 1;
my $parser = DateTime::Format::Strptime->new(
- pattern => '%d.%m.%Y',
+ pattern => '%F',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
);
- if ( $filter_from
- and $filter_from =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x )
- {
+ if ($filter_from) {
$filter_from = $parser->parse_datetime($filter_from);
}
else {
$filter_from = undef;
}
- if ( $filter_until
- and $filter_until =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x )
- {
- $filter_until = $parser->parse_datetime($filter_until);
+ if ($filter_until) {
+ $filter_until = $parser->parse_datetime($filter_until)->set(
+ hour => 23,
+ minute => 59,
+ second => 58
+ );
}
else {
$filter_until = undef;
}
+ my $year;
+ if ( $filter_from
+ and $filter_from->day == 1
+ and $filter_from->month == 1
+ and $filter_until
+ and $filter_until->day == 31
+ and $filter_until->month == 12
+ and $filter_from->year == $filter_until->year )
+ {
+ $year = $filter_from->year;
+ }
+
my @journeys = $self->journeys->get(
uid => $self->current_user->{id},
with_polyline => $with_polyline,
@@ -1353,7 +1820,9 @@ sub map_history {
$self->render(
template => 'history_map',
+ year => $year,
with_map => 1,
+ title => 'travelynx: Karte',
%{$res}
);
}
@@ -1372,15 +1841,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;
@@ -1397,13 +1870,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}
)
@@ -1452,7 +1929,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;
@@ -1479,7 +1956,7 @@ sub year_in_review {
$self->render(
'year_in_review',
- title => "travelynx Jahresrückblick $year",
+ title => "travelynx: Jahresrückblick $year",
year => $year,
stats => $stats,
review => $review,
@@ -1525,7 +2002,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;
}
@@ -1550,6 +2027,7 @@ sub yearly_history {
},
any => {
template => 'history_by_year',
+ title => "travelynx: $year",
journeys => [@journeys],
year => $year,
have_review => $with_review,
@@ -1601,7 +2079,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;
@@ -1613,6 +2091,8 @@ sub monthly_history {
month => $month
);
+ my $month_name = $months[ $month - 1 ];
+
$self->respond_to(
json => {
json => {
@@ -1621,12 +2101,15 @@ sub monthly_history {
}
},
any => {
- template => 'history_by_month',
- journeys => [@journeys],
- year => $year,
- month => $month,
- month_name => $months[ $month - 1 ],
- 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
}
);
@@ -1642,25 +2125,66 @@ sub journey_details {
$self->param( journey_id => $journey_id );
if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) {
- $self->render(
- 'journey',
- status => 404,
- error => 'notfound',
- journey => {}
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404
+ },
+ any => {
+ template => 'journey',
+ status => 404,
+ error => 'notfound',
+ journey => {}
+ }
);
return;
}
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) {
+
+ if ( $self->stash('polyline_export') ) {
+
+ if ( not( $journey->{polyline} and @{ $journey->{polyline} } ) ) {
+ $journey->{polyline}
+ = [ map { [ $_->[2]{lon}, $_->[2]{lat}, $_->[1] ] }
+ @{ $journey->{route} } ];
+ }
+
+ delete $self->stash->{layout};
+
+ my $xml = $self->render_to_string(
+ template => 'polyline',
+ name => sprintf( '%s %s: %s → %s',
+ $journey->{type}, $journey->{no},
+ $journey->{from_name}, $journey->{to_name} ),
+ polyline => $journey->{polyline}
+ );
+ $self->respond_to(
+ gpx => {
+ text => $xml,
+ format => 'gpx'
+ },
+ json => {
+ json => [
+ map {
+ $_->[2] ? [ $_->[0], $_->[1], int( $_->[2] ) ] : $_
+ } @{ $journey->{polyline} }
+ ]
+ },
+ );
+ return;
+ }
+
my $map_data = $self->journeys_to_map_data(
journeys => [$journey],
include_manual => 1,
@@ -1698,23 +2222,39 @@ sub journey_details {
$delay, $journey->{rt_arrival}->strftime('%H:%M') );
}
- $self->render(
- 'journey',
- error => undef,
- journey => $journey,
- journey_visibility => $visibility,
- with_map => 1,
- with_share => $with_share,
- share_text => $share_text,
- %{$map_data},
+ $self->respond_to(
+ json => { json => $journey },
+ any => {
+ template => 'journey',
+ title => sprintf(
+ 'travelynx: Fahrt %s %s %s am %s',
+ $journey->{type},
+ $journey->{line} // '',
+ $journey->{no},
+ $journey->{sched_departure}->strftime('%d.%m.%Y um %H:%M')
+ ),
+ error => undef,
+ journey => $journey,
+ journey_visibility => $visibility,
+ with_map => 1,
+ with_share => $with_share,
+ share_text => $share_text,
+ %{$map_data},
+ }
);
}
else {
- $self->render(
- 'journey',
- status => 404,
- error => 'notfound',
- journey => {}
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404
+ },
+ any => {
+ template => 'journey',
+ status => 404,
+ error => 'notfound',
+ journey => {}
+ }
);
}
@@ -1874,10 +2414,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 ) {
@@ -1893,7 +2434,12 @@ sub edit_journey {
my $error = undef;
if ( $self->param('action') and $self->param('action') eq 'save' ) {
- my $parser = DateTime::Format::Strptime->new(
+ my $parser_sec = DateTime::Format::Strptime->new(
+ pattern => '%d.%m.%Y %H:%M:%S',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
+ my $parser_min = DateTime::Format::Strptime->new(
pattern => '%d.%m.%Y %H:%M',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
@@ -1904,7 +2450,8 @@ sub edit_journey {
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival))
{
- my $datetime = $parser->parse_datetime( $self->param($key) );
+ my $datetime = $parser_sec->parse_datetime( $self->param($key) )
+ // $parser_min->parse_datetime( $self->param($key) );
if ( $datetime and $datetime->epoch ne $journey->{$key}->epoch ) {
$error = $self->journeys->update(
uid => $uid,
@@ -1978,11 +2525,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);
}
@@ -1995,8 +2543,14 @@ sub edit_journey {
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival)) {
if ( $journey->{$key} and $journey->{$key}->epoch ) {
- $self->param(
- $key => $journey->{$key}->strftime('%d.%m.%Y %H:%M') );
+ if ( $journey->{$key}->second ) {
+ $self->param(
+ $key => $journey->{$key}->strftime('%d.%m.%Y %H:%M:%S') );
+ }
+ else {
+ $self->param(
+ $key => $journey->{$key}->strftime('%d.%m.%Y %H:%M') );
+ }
}
}
@@ -2016,17 +2570,210 @@ sub edit_journey {
$self->render(
'edit_journey',
with_autocomplete => 1,
+ backend_id => $journey->{backend_id},
error => $error,
journey => $journey
);
}
+# Taken from Travel::Status::DE::EFA::Trip#polyline
+sub polyline_add_stops {
+ my ( $self, %opt ) = @_;
+
+ my $polyline = $opt{polyline};
+ my $route = $opt{route};
+
+ my $distance = GIS::Distance->new;
+
+ my %min_dist;
+ my $route_i = 0;
+ for my $stop ( @{$route} ) {
+ for my $polyline_index ( 0 .. $#{$polyline} ) {
+ my $pl = $polyline->[$polyline_index];
+ my $dist
+ = $distance->distance_metal( $stop->[2]{lat}, $stop->[2]{lon},
+ $pl->[1], $pl->[0] );
+ my $key = $route_i . ';' . $stop->[1];
+ if ( not $min_dist{$key}
+ or $min_dist{$key}{dist} > $dist )
+ {
+ $min_dist{$key} = {
+ dist => $dist,
+ index => $polyline_index,
+ };
+ }
+ }
+ $route_i += 1;
+ }
+ $route_i = 0;
+ for my $stop ( @{$route} ) {
+ my $key = $route_i . ';' . $stop->[1];
+ if ( $min_dist{$key} ) {
+ if ( defined $polyline->[ $min_dist{$key}{index} ][2] ) {
+ return sprintf(
+ 'Error: Stop IDs %d and %d both map to lon %f, lat %f',
+ $polyline->[ $min_dist{$key}{index} ][2],
+ $stop->[1],
+ $polyline->[ $min_dist{$key}{index} ][0],
+ $polyline->[ $min_dist{$key}{index} ][1]
+ );
+ }
+ $polyline->[ $min_dist{$key}{index} ][2]
+ = $stop->[1];
+ }
+ $route_i += 1;
+ }
+ return;
+}
+
+sub set_polyline {
+ my ($self) = @_;
+
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
+
+ my $journey_id = $self->param('id');
+ my $uid = $self->current_user->{id};
+
+ # Ensure that the journey exists and belongs to the user
+ my $journey = $self->journeys->get_single(
+ uid => $uid,
+ journey_id => $journey_id,
+ );
+
+ if ( not $journey ) {
+ $self->render(
+ 'bad_request',
+ message => 'Invalid journey ID',
+ status => 400,
+ );
+ return;
+ }
+
+ if ( my $upload = $self->req->upload('file') ) {
+ my $root;
+ eval {
+ $root = XML::LibXML->load_xml( string => $upload->asset->slurp );
+ };
+
+ if ($@) {
+ $self->render(
+ 'bad_request',
+ message => "Invalid GPX file: Invalid XML: $@",
+ status => 400,
+ );
+ return;
+ }
+
+ my $context = XML::LibXML::XPathContext->new($root);
+ $context->registerNs( 'gpx', 'http://www.topografix.com/GPX/1/1' );
+
+ use Data::Dumper;
+
+ my @polyline;
+ for my $point (
+ $context->findnodes('/gpx:gpx/gpx:trk/gpx:trkseg/gpx:trkpt') )
+ {
+ push(
+ @polyline,
+ [
+ 0.0 + $point->getAttribute('lon'),
+ 0.0 + $point->getAttribute('lat')
+ ]
+ );
+ }
+
+ if ( not @polyline ) {
+ $self->render(
+ 'bad_request',
+ message => 'Invalid GPX file: found no track points',
+ status => 400,
+ );
+ return;
+ }
+
+ my @route = @{ $journey->{route} };
+
+ if ( $self->param('upload-partial') ) {
+ my $route_start = first_index {
+ (
+ (
+ $_->[1] and $_->[1] == $journey->{from_eva}
+ or $_->[0] eq $journey->{from_name}
+ )
+ and (
+ not( defined $_->[2]{sched_dep}
+ or defined $_->[2]{rt_dep} )
+ or ( $_->[2]{sched_dep} // $_->[2]{rt_dep} )
+ == $journey->{sched_dep_ts}
+ )
+ )
+ }
+ @route;
+
+ my $route_end = last_index {
+ (
+ (
+ $_->[1] and $_->[1] == $journey->{to_eva}
+ or $_->[0] eq $journey->{to_name}
+ )
+ and (
+ not( defined $_->[2]{sched_arr}
+ or defined $_->[2]{rt_arr} )
+ or ( $_->[2]{sched_arr} // $_->[2]{rt_arr} )
+ == $journey->{sched_arr_ts}
+ )
+ )
+ }
+ @route;
+
+ if ( $route_start > -1 and $route_end > -1 ) {
+ @route = @route[ $route_start .. $route_end ];
+ }
+ }
+
+ my $err = $self->polyline_add_stops(
+ polyline => \@polyline,
+ route => \@route,
+ );
+
+ if ($err) {
+ $self->render(
+ 'bad_request',
+ message => $err,
+ status => 400,
+ );
+ return;
+ }
+
+ $self->journeys->set_polyline(
+ uid => $uid,
+ journey_id => $journey_id,
+ edited => $journey->{edited},
+ polyline => \@polyline,
+ from_eva => $route[0][1],
+ to_eva => $route[-1][1],
+ stats_ts => $journey->{rt_dep_ts},
+ );
+ }
+
+ $self->redirect_to("/journey/${journey_id}");
+}
+
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',
+ pattern => '%FT%H:%M',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
);
@@ -2044,8 +2791,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;
}
@@ -2058,6 +2806,7 @@ sub add_journey_form {
$self->render(
'add_journey',
with_autocomplete => 1,
+ status => 400,
error => "${key}: Ungültiges Datums-/Zeitformat"
);
return;
@@ -2080,8 +2829,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);
@@ -2099,6 +2849,7 @@ sub add_journey_form {
$self->render(
'add_journey',
with_autocomplete => 1,
+ status => 400,
error => $error,
);
}
@@ -2116,4 +2867,269 @@ 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 => '%FT%H:%M',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
+ my $time_parser = DateTime::Format::Strptime->new(
+ pattern => '%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_ts = $trip{sched_departure};
+ for my $station ( @{ $trip{route} } ) {
+ my $ts;
+ my %station_data;
+ if ( $station
+ =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x
+ )
+ {
+ $station = $+{stop};
+
+ # attempt to parse "07:08" short timestamp first
+ $ts = $time_parser->parse_datetime( $+{timestamp} );
+ if ($ts) {
+
+ # fill in last stop's (or at the first stop, our departure's)
+ # date to complete the datetime
+ $ts = $ts->set(
+ year => $prev_ts->year,
+ month => $prev_ts->month,
+ day => $prev_ts->day
+ );
+
+ # if we go back in time with this, assume we went
+ # over midnight and add a day, e.g. in case of a stop
+ # at 23:00 followed by one at 01:30
+ if ( $ts < $prev_ts ) {
+ $ts = $ts->add( days => 1 );
+ }
+ }
+ else {
+ # do a full datetime parse
+ $ts = $parser->parse_datetime( $+{timestamp} );
+ }
+ if ( $ts and $ts >= $prev_ts ) {
+ $station_data{sched_arr} = $ts->epoch;
+ $station_data{sched_dep} = $ts->epoch;
+ $prev_ts = $ts;
+ }
+ 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;