travelynx/lib/Travelynx/Controller/Traveling.pm

2191 lines
50 KiB
Perl
Raw Normal View History

package Travelynx::Controller::Traveling;
2023-07-03 15:59:25 +00:00
# Copyright (C) 2020-2023 Birte Kristina Friesel
2020-11-27 21:12:56 +00:00
#
2021-01-29 17:32:13 +00:00
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
2019-03-27 20:20:59 +00:00
use DateTime;
use DateTime::Format::Strptime;
use List::Util qw(uniq min max);
use List::UtilsBy qw(max_by uniq_by);
2019-11-16 20:24:35 +00:00
use List::MoreUtils qw(first_index);
2022-08-01 08:07:24 +00:00
use Mojo::Promise;
2020-04-19 16:26:20 +00:00
use Text::CSV;
use Travel::Status::DE::IRIS::Stations;
# Internal Helpers
sub has_str_in_list {
my ( $str, @strs ) = @_;
if ( List::Util::any { $str eq $_ } @strs ) {
return 1;
}
return;
}
2022-08-01 08:07:24 +00:00
sub get_connecting_trains_p {
my ( $self, %opt ) = @_;
my $uid = $opt{uid} //= $self->current_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;
my ( $stationinfo, $arr_epoch, $arr_platform, $arr_countdown );
2022-08-01 08:07:24 +00:00
my $promise = Mojo::Promise->new;
if ( $opt{eva} ) {
if ( $use_history & 0x01 ) {
$eva = $opt{eva};
}
elsif ( $opt{destination_name} ) {
$eva = $opt{eva};
}
}
else {
if ( $use_history & 0x02 ) {
my $status = $self->get_user_status;
$eva = $status->{arr_eva};
$exclude_via = $status->{dep_name};
$exclude_train_id = $status->{train_id};
$arr_platform = $status->{arr_platform};
$stationinfo = $status->{extra_data}{stationinfo_arr};
if ( $status->{real_arrival} ) {
$exclude_before = $arr_epoch = $status->{real_arrival}->epoch;
$arr_countdown = $status->{arrival_countdown};
}
}
}
$exclude_before //= $now - 300;
if ( not $eva ) {
2022-08-01 08:07:24 +00:00
return $promise->reject;
}
my ( $dest_ids, $destinations )
= $self->journeys->get_connection_targets(%opt);
my @destinations = uniq_by { $_->{name} } @{$destinations};
if ($exclude_via) {
@destinations = grep { $_->{name} ne $exclude_via } @destinations;
}
if ( not @destinations ) {
2022-08-01 08:07:24 +00:00
return $promise->reject;
}
my $iris_eva = $eva;
if ( $eva < 8000000 ) {
$iris_eva = ( List::Util::first { $_ >= 8000000 } @{$dest_ids} )
// $eva;
}
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 ( $iris_eva >= 8000000
and List::Util::any { $_->{eva} >= 8000000 } @destinations )
{
$self->iris->get_departures_p(
station => $iris_eva,
lookbehind => 10,
lookahead => $lookahead,
with_related => 1
)->then(
sub {
my ($stationboard) = @_;
if ( $stationboard->{errstr} ) {
$iris_promise->resolve( [] );
return;
2022-08-01 08:07:24 +00:00
}
@{ $stationboard->{results} } = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [ $_, $_->departure ? $_->departure->epoch : 0 ] }
@{ $stationboard->{results} };
my @results;
my @cancellations;
my $excluded_train;
for my $train ( @{ $stationboard->{results} } ) {
if ( not $train->departure ) {
next;
}
if ( $exclude_before
and $train->departure
and $train->departure->epoch < $exclude_before )
{
next;
}
if ( $exclude_train_id
and $train->train_id eq $exclude_train_id )
{
$excluded_train = $train;
next;
}
2023-07-15 14:11:21 +00:00
# In general, this function is meant to return feasible
# connections. However, cancelled connections may also be of
# interest and are also useful for logging cancellations.
# To satisfy both demands with (hopefully) little confusion and
# UI clutter, this function returns two concatenated arrays:
# actual connections (ordered by actual departure time) followed
# by cancelled connections (ordered by scheduled departure time).
# This is easiest to achieve in two separate loops.
#
# Note that a cancelled train may still have a matching destination
# in its route_post, e.g. if it leaves out $eva due to
# unscheduled route changes but continues on schedule afterwards
# -- so it is only cancelled at $eva, not on the remainder of
# the route. Also note that this specific case is not yet handled
# properly by the cancellation logic etc.
2022-08-01 08:07:24 +00:00
if ( $train->departure_is_cancelled ) {
my @via = (
$train->sched_route_post, $train->sched_route_end
);
for my $dest (@destinations) {
if ( has_str_in_list( $dest->{name}, @via ) ) {
push( @cancellations, [ $train, $dest ] );
next;
}
2022-08-01 08:07:24 +00:00
}
}
else {
my @via = ( $train->route_post, $train->route_end );
for my $dest (@destinations) {
if ( $via_count{ $dest->{name} } < 2
and has_str_in_list( $dest->{name}, @via ) )
{
push( @results, [ $train, $dest ] );
2023-07-15 14:11:21 +00:00
# Show all past and up to two future departures per destination
if ( not $train->departure
or $train->departure->epoch >= $now )
{
$via_count{ $dest->{name} }++;
}
next;
2022-08-01 08:07:24 +00:00
}
}
}
}
@results = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map {
[
$_,
$_->[0]->departure->epoch
// $_->[0]->sched_departure->epoch
]
} @results;
@cancellations = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [ $_, $_->[0]->sched_departure->epoch ] }
@cancellations;
# remove trains whose route matches the excluded one's
if ($excluded_train) {
my $route_pre
= join( '|', reverse $excluded_train->route_pre );
@results
= grep { join( '|', $_->[0]->route_post ) ne $route_pre }
@results;
my $route_post = join( '|', $excluded_train->route_post );
@results
= grep { join( '|', $_->[0]->route_post ) ne $route_post }
@results;
2022-08-01 08:07:24 +00:00
}
# add message IDs and 'transfer short' hints
for my $result (@results) {
my $train = $result->[0];
my @message_ids
= List::Util::uniq map { $_->[1] } $train->raw_messages;
$train->{message_id} = { map { $_ => 1 } @message_ids };
my $interchange_duration;
if ( exists $stationinfo->{i} ) {
if ( defined $arr_platform
and defined $train->platform )
{
$interchange_duration
= $stationinfo->{i}{$arr_platform}
{ $train->platform };
}
$interchange_duration //= $stationinfo->{i}{"*"};
2022-08-01 08:07:24 +00:00
}
if ( defined $interchange_duration ) {
my $interchange_time
= ( $train->departure->epoch - $arr_epoch ) / 60;
if ( $interchange_time < $interchange_duration ) {
$train->{interchange_text} = 'Anschluss knapp';
$train->{interchange_icon} = 'directions_run';
}
elsif ( $interchange_time == $interchange_duration ) {
$train->{interchange_text}
= 'Anschluss könnte knapp werden';
$train->{interchange_icon} = 'directions_run';
}
2022-08-01 08:07:24 +00:00
}
}
$iris_promise->resolve( [ @results, @cancellations ] );
return;
}
)->catch(
sub {
$iris_promise->resolve( [] );
return;
}
)->wait;
}
else {
$iris_promise->resolve( [] );
}
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;
}
}
2022-08-01 08:07:24 +00:00
}
for my $hafas_train (@all_hafas_trains) {
if ( $hafas_train->{iris_seen} ) {
next;
}
2023-10-01 08:51:58 +00:00
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
and $stop->loc->name eq $dest->{name}
and $via_count{ $dest->{name} } < 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->{name} }++;
push( @hafas_trains,
[ $hafas_train, $dest, $arrival ] );
}
}
}
}
}
};
if ($@) {
$self->app->log->error(
"get_connecting_trains_p($uid): IRIS/HAFAS merge failed: $@"
);
2022-08-01 08:07:24 +00:00
}
$promise->resolve( \@iris_trains, \@hafas_trains );
return;
2022-08-01 08:07:24 +00:00
}
)->catch(
sub {
my ($err) = @_;
$promise->reject($err);
2022-08-01 08:07:24 +00:00
return;
}
)->wait;
2022-08-01 08:07:24 +00:00
return $promise;
}
sub compute_effective_visibility {
my ( $self, $default_visibility, $journey_visibility ) = @_;
if ( $journey_visibility eq 'default' ) {
return $default_visibility;
}
return $journey_visibility;
}
# Controllers
sub homepage {
my ($self) = @_;
if ( $self->is_user_authenticated ) {
2023-07-15 17:20:37 +00:00
my $uid = $self->current_user->{id};
my $status = $self->get_user_status;
my @timeline = $self->in_transit->get_timeline(
uid => $uid,
short => 1
);
$self->stash( timeline => [@timeline] );
my @recent_targets;
if ( $status->{checked_in} ) {
my $journey_visibility
= $self->compute_effective_visibility(
$self->current_user->{default_visibility_str},
$status->{visibility_str} );
if ( defined $status->{arrival_countdown}
and $status->{arrival_countdown} < ( 40 * 60 ) )
{
2022-08-01 08:07:24 +00:00
$self->render_later;
$self->get_connecting_trains_p->then(
sub {
my ( $connections_iris, $connections_hafas ) = @_;
2022-08-01 08:07:24 +00:00
$self->render(
'landingpage',
user_status => $status,
journey_visibility => $journey_visibility,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
2022-08-01 08:07:24 +00:00
);
2023-07-16 08:30:23 +00:00
$self->users->mark_seen( uid => $uid );
2022-08-01 08:07:24 +00:00
}
)->catch(
sub {
$self->render(
'landingpage',
user_status => $status,
journey_visibility => $journey_visibility,
2022-08-01 08:07:24 +00:00
);
2023-07-16 08:30:23 +00:00
$self->users->mark_seen( uid => $uid );
2022-08-01 08:07:24 +00:00
}
)->wait;
return;
}
else {
$self->render(
'landingpage',
2023-07-16 08:30:23 +00:00
user_status => $status,
journey_visibility => $journey_visibility,
);
2023-07-16 08:30:23 +00:00
$self->users->mark_seen( uid => $uid );
return;
}
}
else {
2023-01-22 09:32:06 +00:00
@recent_targets = uniq_by { $_->{eva} }
2023-07-16 08:30:23 +00:00
$self->journeys->get_latest_checkout_stations( uid => $uid );
}
$self->render(
'landingpage',
user_status => $status,
recent_targets => \@recent_targets,
with_autocomplete => 1,
with_geolocation => 1
);
2023-07-16 08:30:23 +00:00
$self->users->mark_seen( uid => $uid );
}
else {
2023-07-16 08:30:23 +00:00
$self->render( 'landingpage', intro => 1 );
}
}
sub status_card {
my ($self) = @_;
my $status = $self->get_user_status;
delete $self->stash->{layout};
2023-07-15 17:20:37 +00:00
my @timeline = $self->in_transit->get_timeline(
uid => $self->current_user->{id},
short => 1
);
$self->stash( timeline => [@timeline] );
if ( $status->{checked_in} ) {
my $journey_visibility
= $self->compute_effective_visibility(
$self->current_user->{default_visibility_str},
$status->{visibility_str} );
if ( defined $status->{arrival_countdown}
and $status->{arrival_countdown} < ( 40 * 60 ) )
{
2022-08-01 08:07:24 +00:00
$self->render_later;
$self->get_connecting_trains_p->then(
sub {
my ( $connections_iris, $connections_hafas ) = @_;
2022-08-01 08:07:24 +00:00
$self->render(
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
2022-08-01 08:07:24 +00:00
);
}
)->catch(
sub {
$self->render(
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
);
2022-08-01 08:07:24 +00:00
}
)->wait;
return;
}
$self->render(
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
);
}
elsif ( $status->{cancellation} ) {
2022-08-01 08:07:24 +00:00
$self->render_later;
$self->get_connecting_trains_p(
eva => $status->{cancellation}{dep_eva},
destination_name => $status->{cancellation}{arr_name}
2022-08-01 08:07:24 +00:00
)->then(
sub {
my ($connecting_trains) = @_;
2022-08-01 08:07:24 +00:00
$self->render(
'_cancelled_departure',
journey => $status->{cancellation},
connections_iris => $connecting_trains
2022-08-01 08:07:24 +00:00
);
}
)->catch(
sub {
$self->render( '_cancelled_departure',
journey => $status->{cancellation} );
}
)->wait;
return;
}
else {
my @connecting_trains;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
if ( $now->epoch - $status->{timestamp}->epoch < ( 30 * 60 ) ) {
2022-08-01 08:07:24 +00:00
$self->render_later;
$self->get_connecting_trains_p->then(
sub {
my ( $connections_iris, $connections_hafas ) = @_;
2022-08-01 08:07:24 +00:00
$self->render(
'_checked_out',
journey => $status,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
2022-08-01 08:07:24 +00:00
);
}
)->catch(
sub {
$self->render( '_checked_out', journey => $status );
}
)->wait;
return;
}
2022-08-01 08:07:24 +00:00
$self->render( '_checked_out', journey => $status );
}
}
sub geolocation {
my ($self) = @_;
my $lon = $self->param('lon');
my $lat = $self->param('lat');
if ( not $lon or not $lat ) {
$self->render( json => { error => 'Invalid lon/lat received' } );
return;
}
$self->render_later;
my @iris = map {
{
ds100 => $_->[0][0],
name => $_->[0][1],
eva => $_->[0][2],
lon => $_->[0][3],
lat => $_->[0][4],
distance => $_->[1],
hafas => 0,
}
} Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon,
$lat, 10 );
@iris = uniq_by { $_->{name} } @iris;
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;
2023-08-20 14:52:09 +00:00
if ( @hafas > 10 ) {
@hafas = @hafas[ 0 .. 9 ];
}
2023-08-20 14:52:09 +00:00
my @results = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [ $_, $_->{distance} ] } ( @iris, @hafas );
$self->render(
json => {
2023-08-20 14:52:09 +00:00
candidates => [@results],
}
);
}
)->catch(
sub {
my ($err) = @_;
$self->render(
json => {
candidates => [@iris],
warning => $err,
}
);
}
)->wait;
}
sub travel_action {
my ($self) = @_;
my $params = $self->req->json;
if ( not exists $params->{action} ) {
$params = $self->req->params->to_hash;
}
if ( not $self->is_user_authenticated ) {
# We deliberately do not set the HTTP status for these replies, as it
# confuses jquery.
$self->render(
json => {
success => 0,
error => 'Session error, please login again',
},
);
return;
}
if ( not $params->{action} ) {
$self->render(
json => {
success => 0,
error => 'Missing action value',
},
);
return;
}
my $station = $params->{station};
if ( $params->{action} eq 'checkin' ) {
my $status = $self->get_user_status;
my $promise;
if ( $status->{checked_in}
and $status->{arr_eva}
and $status->{arrival_countdown} <= 0 )
{
$promise = $self->checkout_p( station => $status->{arr_eva} );
}
else {
$promise = Mojo::Promise->resolve;
}
$self->render_later;
$promise->then(
sub {
return $self->checkin_p(
station => $params->{station},
train_id => $params->{train}
);
}
)->then(
sub {
my $destination = $params->{dest};
if ( not $destination ) {
$self->render(
json => {
success => 1,
redirect_to => '/',
},
);
return;
}
2023-07-15 14:11:21 +00:00
# Silently ignore errors -- if they are permanent, the user will see
# them when selecting the destination manually.
return $self->checkout_p(
station => $destination,
force => 0
);
}
)->then(
sub {
my ( $still_checked_in, undef ) = @_;
if ( my $destination = $params->{dest} ) {
my $station_link = '/s/' . $destination;
if ( $status->{train_id} =~ m{[|]} ) {
$station_link .= '?hafas=1';
}
$self->render(
json => {
success => 1,
redirect_to => $still_checked_in
? '/'
: $station_link,
},
);
}
return;
}
)->catch(
sub {
my ($error) = @_;
$self->render(
json => {
success => 0,
error => $error,
},
);
}
)->wait;
}
elsif ( $params->{action} eq 'checkout' ) {
$self->render_later;
my $status = $self->get_user_status;
$self->checkout_p(
station => $params->{station},
force => $params->{force}
)->then(
sub {
my ( $still_checked_in, $error ) = @_;
my $station_link = '/s/' . $params->{station};
if ( $status->{train_id} =~ m{[|]} ) {
$station_link .= '?hafas=1';
}
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
else {
$self->render(
json => {
success => 1,
redirect_to => $still_checked_in
? '/'
: $station_link,
},
);
}
return;
}
)->catch(
sub {
my ($error) = @_;
$self->render(
json => {
success => 0,
error => $error,
},
);
return;
}
)->wait;
}
elsif ( $params->{action} eq 'undo' ) {
my $status = $self->get_user_status;
2019-04-26 17:53:01 +00:00
my $error = $self->undo( $params->{undo_id} );
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
else {
my $redir = '/';
2019-04-26 17:53:01 +00:00
if ( $status->{checked_in} or $status->{cancelled} ) {
if ( $status->{train_id} =~ m{[|]} ) {
$redir = '/s/' . $status->{dep_eva} . '?hafas=1';
2023-08-13 10:51:15 +00:00
}
else {
$redir = '/s/' . $status->{dep_ds100};
2023-08-13 10:51:15 +00:00
}
}
$self->render(
json => {
success => 1,
redirect_to => $redir,
},
);
}
}
elsif ( $params->{action} eq 'cancelled_from' ) {
$self->render_later;
$self->checkin_p(
station => $params->{station},
train_id => $params->{train}
)->then(
sub {
$self->render(
json => {
success => 1,
redirect_to => '/',
},
);
}
)->catch(
sub {
my ($error) = @_;
$self->render(
json => {
success => 0,
error => $error,
},
);
}
)->wait;
}
elsif ( $params->{action} eq 'cancelled_to' ) {
$self->render_later;
$self->checkout_p(
station => $params->{station},
force => 1
)->then(
sub {
my ( undef, $error ) = @_;
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
else {
$self->render(
json => {
success => 1,
redirect_to => '/',
},
);
}
return;
}
)->catch(
sub {
my ($error) = @_;
$self->render(
json => {
success => 0,
error => $error,
},
);
return;
}
)->wait;
}
2019-04-04 16:26:53 +00:00
elsif ( $params->{action} eq 'delete' ) {
my $error = $self->journeys->delete(
uid => $self->current_user->{id},
id => $params->{id},
checkin => $params->{checkin},
checkout => $params->{checkout}
);
2019-04-04 16:26:53 +00:00
if ($error) {
$self->render(
json => {
success => 0,
error => $error,
},
);
}
else {
$self->render(
json => {
success => 1,
redirect_to => '/history',
2019-04-04 16:26:53 +00:00
},
);
}
}
else {
$self->render(
json => {
success => 0,
error => 'invalid action value',
},
);
}
}
sub station {
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 $uid = $self->current_user->{id};
my @timeline = $self->in_transit->get_timeline(
uid => $uid,
short => 1
);
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 );
2022-07-26 08:41:44 +00:00
$self->render_later;
2023-03-27 19:03:25 +00:00
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' );
}
2023-03-27 19:03:25 +00:00
my $use_hafas = $self->param('hafas');
my $promise;
if ($use_hafas) {
$promise = $self->hafas->get_departures_p(
eva => $station,
timestamp => $timestamp,
2023-08-13 10:51:15 +00:00
lookbehind => 30,
2023-03-27 19:03:25 +00:00
lookahead => 30,
);
}
else {
$promise = $self->iris->get_departures_p(
station => $station,
lookbehind => 120,
lookahead => 30,
with_related => 1,
);
}
$promise->then(
2022-07-26 08:41:44 +00:00
sub {
my ($status) = @_;
2023-08-15 06:37:14 +00:00
my $api_link;
2023-03-27 19:03:25 +00:00
my @results;
2023-08-15 06:37:14 +00:00
my $now = $self->now->epoch;
my $now_within_range
= abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0;
2023-03-27 19:03:25 +00:00
if ($use_hafas) {
2023-08-15 06:37:14 +00:00
my $iris_eva = List::Util::min grep { $_ >= 1000000 }
@{ $status->station->{evas} // [] };
if ($iris_eva) {
$api_link = '/s/' . $iris_eva;
}
2023-03-27 19:03:25 +00:00
@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} // []
);
2023-03-27 19:03:25 +00:00
$status = {
station_eva => $status->station->{eva},
station_name => (
List::Util::reduce { length($a) < length($b) ? $a : $b }
@{ $status->station->{names} }
),
2023-03-27 19:03:25 +00:00
related_stations => [],
};
}
else {
2023-08-15 06:37:14 +00:00
$api_link = '/s/' . $status->{station_eva} . '?hafas=1';
2023-03-27 19:03:25 +00:00
# You can't check into a train which terminates here
@results = grep { $_->departure } @{ $status->{results} };
2023-03-27 19:03:25 +00:00
@results = map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
map {
[ $_, $_->departure->epoch // $_->sched_departure->epoch ]
} @results;
}
my $user_status = $self->get_user_status;
my $can_check_out = 0;
if ( $user_status->{checked_in} ) {
for my $stop ( @{ $user_status->{route_after} } ) {
if (
$stop->[1] eq $status->{station_eva}
or List::Util::any { $stop->[1] eq $_->{uic} }
@{ $status->{related_stations} }
)
{
$can_check_out = 1;
last;
}
}
}
2022-08-01 08:07:24 +00:00
my $connections_p;
if ( $trip_id and $use_hafas ) {
@results = grep { $_->id eq $trip_id } @results;
}
elsif ( $train and not $use_hafas ) {
2022-07-26 08:41:44 +00:00
@results
= grep { $_->type . ' ' . $_->train_no eq $train } @results;
}
else {
if ( $user_status->{cancellation}
and $status->{station_eva} eq
$user_status->{cancellation}{dep_eva} )
{
2022-08-01 08:07:24 +00:00
$connections_p = $self->get_connecting_trains_p(
eva => $user_status->{cancellation}{dep_eva},
destination_name =>
$user_status->{cancellation}{arr_name}
);
}
else {
2022-08-01 08:07:24 +00:00
$connections_p = $self->get_connecting_trains_p(
eva => $status->{station_eva} );
}
}
2022-08-01 08:07:24 +00:00
if ($connections_p) {
$connections_p->then(
sub {
my ( $connections_iris, $connections_hafas ) = @_;
2022-08-01 08:07:24 +00:00
$self->render(
'departures',
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,
2023-07-16 08:30:23 +00:00
title => "travelynx: $status->{station_name}",
2022-08-01 08:07:24 +00:00
);
}
)->catch(
sub {
$self->render(
'departures',
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
2022-08-01 08:07:24 +00:00
results => \@results,
2023-03-27 19:03:25 +00:00
hafas => $use_hafas,
2022-08-01 08:07:24 +00:00
station => $status->{station_name},
related_stations => $status->{related_stations},
user_status => $user_status,
can_check_out => $can_check_out,
2023-08-15 06:37:14 +00:00
api_link => $api_link,
2023-07-16 08:30:23 +00:00
title => "travelynx: $status->{station_name}",
2022-08-01 08:07:24 +00:00
);
}
)->wait;
}
else {
$self->render(
'departures',
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
2022-08-01 08:07:24 +00:00
results => \@results,
2023-03-27 19:03:25 +00:00
hafas => $use_hafas,
2022-08-01 08:07:24 +00:00
station => $status->{station_name},
related_stations => $status->{related_stations},
user_status => $user_status,
can_check_out => $can_check_out,
2023-08-15 06:37:14 +00:00
api_link => $api_link,
2022-08-01 08:07:24 +00:00
title => "travelynx: $status->{station_name}",
);
}
2022-07-26 08:41:44 +00:00
}
)->catch(
sub {
my ( $err, $status ) = @_;
if ( $status and $status->{suggestions} ) {
$self->render(
'disambiguation',
suggestions => $status->{suggestions},
status => 300,
);
}
elsif ( $use_hafas and $status and $status->errcode eq 'LOCATION' )
{
$self->hafas->search_location_p( query => $station )->then(
sub {
my ($hafas2) = @_;
my @suggestions = $hafas2->results;
if ( @suggestions == 1 ) {
$self->redirect_to(
'/s/' . $suggestions[0]->eva . '?hafas=1' );
}
else {
$self->render(
'disambiguation',
suggestions => [
map { { name => $_->name, eva => $_->eva } }
@suggestions
],
status => 300,
);
}
}
)->catch(
sub {
my ($err2) = @_;
$self->render(
'exception',
exception =>
"locationSearch threw '$err2' when handling '$err'",
status => 502
);
}
)->wait;
}
else {
$self->render(
'exception',
exception => $err,
status => 502
);
}
2022-07-26 08:41:44 +00:00
}
)->wait;
2023-07-16 08:30:23 +00:00
$self->users->mark_seen( uid => $uid );
}
sub redirect_to_station {
my ($self) = @_;
my $station = $self->param('station');
2023-08-18 18:42:37 +00:00
if ( my $s = $self->app->stations->search($station) ) {
if ( $s->{source} == 1 ) {
2023-08-18 18:42:37 +00:00
$self->redirect_to("/s/${station}?hafas=1");
}
else {
$self->redirect_to("/s/${station}");
}
}
else {
$self->redirect_to("/s/${station}?hafas=1");
}
}
sub cancelled {
2019-03-22 15:56:49 +00:00
my ($self) = @_;
my @journeys = $self->journeys->get(
uid => $self->current_user->{id},
cancelled => 1,
with_datetime => 1
);
2019-03-27 20:20:59 +00:00
2019-03-22 15:56:49 +00:00
$self->respond_to(
2019-03-27 20:20:59 +00:00
json => { json => [@journeys] },
any => {
template => 'cancelled',
2019-03-27 20:20:59 +00:00
journeys => [@journeys]
}
2019-03-22 15:56:49 +00:00
);
}
sub history {
my ($self) = @_;
2024-01-30 20:07:57 +00:00
$self->render(
template => 'history',
title => 'travelynx: History'
);
}
sub commute {
my ($self) = @_;
my $year = $self->param('year');
my $filter_type = $self->param('filter_type') || 'exact';
my $station = $self->param('station');
# DateTime is very slow when looking far into the future due to DST changes
# -> Limit time range to avoid accidental DoS.
if (
not( $year
and $year =~ m{ ^ [0-9]{4} $ }x
and $year > 1990
and $year < 2100 )
)
{
$year = DateTime->now( time_zone => 'Europe/Berlin' )->year - 1;
}
my $interval_start = DateTime->new(
time_zone => 'Europe/Berlin',
year => $year,
month => 1,
day => 1,
hour => 0,
minute => 0,
second => 0,
);
my $interval_end = $interval_start->clone->add( years => 1 );
my @journeys = $self->journeys->get(
uid => $self->current_user->{id},
after => $interval_start,
before => $interval_end,
with_datetime => 1,
);
if ( not $station ) {
my %candidate_count;
for my $journey (@journeys) {
my $dep = $journey->{rt_departure};
my $arr = $journey->{rt_arrival};
if ( $arr->dow <= 5 and $arr->hour <= 12 ) {
$candidate_count{ $journey->{to_name} }++;
}
elsif ( $dep->dow <= 5 and $dep->hour > 12 ) {
$candidate_count{ $journey->{from_name} }++;
}
else {
2023-07-15 14:11:21 +00:00
# Avoid selecting an intermediate station for multi-leg commutes.
# Assumption: The intermediate station is also used for private
# travels -> penalize stations which are used on weekends or at
# unexpected times.
$candidate_count{ $journey->{from_name} }--;
$candidate_count{ $journey->{to_name} }--;
}
}
$station = max_by { $candidate_count{$_} } keys %candidate_count;
}
my %journeys_by_month;
my %count_by_month;
my $total = 0;
my $prev_doy = 0;
for my $journey ( reverse @journeys ) {
my $month = $journey->{rt_departure}->month;
if (
(
$filter_type eq 'exact' and ( $journey->{to_name} eq $station
or $journey->{from_name} eq $station )
)
or (
$filter_type eq 'substring'
and ( $journey->{to_name} =~ m{\Q$station\E}
or $journey->{from_name} =~ m{\Q$station\E} )
)
or (
$filter_type eq 'regex'
and ( $journey->{to_name} =~ m{$station}
or $journey->{from_name} =~ m{$station} )
)
)
{
push( @{ $journeys_by_month{$month} }, $journey );
my $doy = $journey->{rt_departure}->day_of_year;
if ( $doy != $prev_doy ) {
$count_by_month{$month}++;
$total++;
}
$prev_doy = $doy;
}
}
$self->param( year => $year );
$self->param( filter_type => $filter_type );
$self->param( station => $station );
$self->render(
template => 'commute',
with_autocomplete => 1,
journeys_by_month => \%journeys_by_month,
count_by_month => \%count_by_month,
total_journeys => $total,
2024-01-30 20:07:57 +00:00
title => 'travelynx: Reisen nach Station',
months => [
qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
],
);
}
sub map_history {
my ($self) = @_;
my $location = $self->app->coordinates_by_station;
2020-01-31 17:16:00 +00:00
if ( not $self->param('route_type') ) {
$self->param( route_type => 'polybee' );
}
my $route_type = $self->param('route_type');
my $filter_from = $self->param('filter_from');
my $filter_until = $self->param('filter_to');
my $filter_type = $self->param('filter_type');
2020-01-31 17:16:00 +00:00
my $with_polyline = $route_type eq 'beeline' ? 0 : 1;
2022-04-02 13:24:39 +00:00
my $parser = DateTime::Format::Strptime->new(
pattern => '%d.%m.%Y',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
);
2022-09-07 16:08:27 +00:00
if ( $filter_from
and $filter_from =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x )
2022-04-02 13:24:39 +00:00
{
$filter_from = $parser->parse_datetime($filter_from);
}
else {
$filter_from = undef;
}
if ( $filter_until
and $filter_until =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x )
{
2023-12-31 11:24:51 +00:00
$filter_until = $parser->parse_datetime($filter_until)->set(
hour => 23,
minute => 59,
second => 58
);
2022-04-02 13:24:39 +00:00
}
else {
$filter_until = undef;
}
2024-01-01 11:58:59 +00:00
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},
2022-04-02 13:24:39 +00:00
with_polyline => $with_polyline,
after => $filter_from,
before => $filter_until,
);
if ($filter_type) {
my @filter = split( qr{, *}, $filter_type );
2022-09-07 16:08:27 +00:00
@journeys
= grep { has_str_in_list( $_->{type}, @filter ) } @journeys;
}
if ( not @journeys ) {
$self->render(
template => 'history_map',
with_map => 1,
2020-01-25 13:55:51 +00:00
skipped_journeys => [],
station_coordinates => [],
polyline_groups => [],
);
return;
}
my $include_manual = $self->param('include_manual') ? 1 : 0;
my $res = $self->journeys_to_map_data(
journeys => \@journeys,
route_type => $route_type,
include_manual => $include_manual
);
$self->render(
template => 'history_map',
2024-01-01 11:58:59 +00:00
year => $year,
with_map => 1,
2024-01-30 20:07:57 +00:00
title => 'travelynx: Karte',
%{$res}
);
}
2019-03-22 15:56:49 +00:00
sub json_history {
my ($self) = @_;
$self->render(
json => [ $self->journeys->get( uid => $self->current_user->{id} ) ] );
}
2020-04-19 16:26:20 +00:00
sub csv_history {
my ($self) = @_;
my $csv = Text::CSV->new( { eol => "\r\n" } );
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'
);
$buf .= $csv->string;
for my $journey (
$self->journeys->get(
uid => $self->current_user->{id},
with_datetime => 1
)
)
{
2020-04-19 16:26:20 +00:00
if (
$csv->combine(
$journey->{type},
$journey->{line},
$journey->{no},
$journey->{from_name},
$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->{user_data}{comment} // q{},
$journey->{id}
)
)
{
$buf .= $csv->string;
}
}
$self->render(
text => $buf,
format => 'csv'
);
}
2022-12-27 10:07:16 +00:00
sub year_in_review {
my ($self) = @_;
my $year = $self->stash('year');
my @journeys;
# DateTime is very slow when looking far into the future due to DST changes
# -> Limit time range to avoid accidental DoS.
if ( not( $year =~ m{ ^ [0-9]{4} $ }x and $year > 1990 and $year < 2100 ) )
{
$self->render( 'not_found', status => 404 );
2022-12-27 10:07:16 +00:00
return;
}
my $interval_start = DateTime->new(
time_zone => 'Europe/Berlin',
year => $year,
month => 1,
day => 1,
hour => 0,
minute => 0,
second => 0,
);
my $interval_end = $interval_start->clone->add( years => 1 );
@journeys = $self->journeys->get(
uid => $self->current_user->{id},
after => $interval_start,
before => $interval_end,
with_datetime => 1
);
if ( not @journeys ) {
$self->render(
'not_found',
message => 'Keine Zugfahrten im angefragten Jahr gefunden.',
status => 404
);
2022-12-27 10:07:16 +00:00
return;
}
my $now = $self->now;
if (
not( $year < $now->year or ( $now->month == 12 and $now->day == 31 ) ) )
{
$self->render(
'not_found',
2022-12-27 10:07:16 +00:00
message =>
'Der aktuelle Jahresrückblick wird erst zum Jahresende (am 31.12.) freigeschaltet',
status => 404
2022-12-27 10:07:16 +00:00
);
return;
}
my ( $stats, $review ) = $self->journeys->get_stats(
uid => $self->current_user->{id},
year => $year,
review => 1
);
$self->render(
'year_in_review',
title => "travelynx: Jahresrückblick $year",
2023-07-16 08:30:23 +00:00
year => $year,
stats => $stats,
review => $review,
2022-12-27 10:07:16 +00:00
);
}
sub yearly_history {
my ($self) = @_;
my $year = $self->stash('year');
my $filter = $self->param('filter');
my @journeys;
2019-05-10 23:35:57 +00:00
# DateTime is very slow when looking far into the future due to DST changes
# -> Limit time range to avoid accidental DoS.
if ( not( $year =~ m{ ^ [0-9]{4} $ }x and $year > 1990 and $year < 2100 ) )
{
$self->render( 'not_found', status => 404 );
return;
}
my $interval_start = DateTime->new(
time_zone => 'Europe/Berlin',
year => $year,
month => 1,
day => 1,
hour => 0,
minute => 0,
second => 0,
);
my $interval_end = $interval_start->clone->add( years => 1 );
@journeys = $self->journeys->get(
uid => $self->current_user->{id},
after => $interval_start,
before => $interval_end,
with_datetime => 1
);
if ( $filter and $filter eq 'single' ) {
@journeys = $self->journeys->grep_single(@journeys);
}
if ( not @journeys ) {
$self->render(
'not_found',
status => 404,
message => 'Keine Zugfahrten im angefragten Jahr gefunden.'
);
return;
}
2022-12-27 10:07:16 +00:00
my $stats = $self->journeys->get_stats(
uid => $self->current_user->{id},
year => $year
);
2022-12-27 10:07:16 +00:00
my $with_review;
my $now = $self->now;
if ( $year < $now->year or ( $now->month == 12 and $now->day == 31 ) ) {
$with_review = 1;
}
$self->respond_to(
json => {
json => {
journeys => [@journeys],
statistics => $stats
}
},
any => {
2022-12-27 10:07:16 +00:00
template => 'history_by_year',
2024-01-30 20:07:57 +00:00
title => "travelynx: $year",
2022-12-27 10:07:16 +00:00
journeys => [@journeys],
year => $year,
have_review => $with_review,
statistics => $stats
}
);
2019-03-22 15:56:49 +00:00
}
2019-03-27 20:20:59 +00:00
sub monthly_history {
my ($self) = @_;
my $year = $self->stash('year');
my $month = $self->stash('month');
my @journeys;
my @months
= (
qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
);
2019-05-10 23:35:57 +00:00
if (
not( $year =~ m{ ^ [0-9]{4} $ }x
and $year > 1990
and $year < 2100
and $month =~ m{ ^ [0-9]{1,2} $ }x
and $month > 0
and $month < 13 )
)
2019-03-27 20:20:59 +00:00
{
$self->render( 'not_found', status => 404 );
return;
2019-03-27 20:20:59 +00:00
}
my $interval_start = DateTime->new(
time_zone => 'Europe/Berlin',
year => $year,
month => $month,
day => 1,
hour => 0,
minute => 0,
second => 0,
);
my $interval_end = $interval_start->clone->add( months => 1 );
@journeys = $self->journeys->get(
uid => $self->current_user->{id},
after => $interval_start,
before => $interval_end,
with_datetime => 1
);
if ( not @journeys ) {
$self->render(
'not_found',
message => 'Keine Zugfahrten im angefragten Monat gefunden.',
status => 404
);
return;
}
2022-12-27 10:07:16 +00:00
my $stats = $self->journeys->get_stats(
uid => $self->current_user->{id},
year => $year,
month => $month
);
2019-03-27 20:20:59 +00:00
my $month_name = $months[ $month - 1 ];
2019-03-27 20:20:59 +00:00
$self->respond_to(
json => {
json => {
journeys => [@journeys],
statistics => $stats
}
},
any => {
template => 'history_by_month',
2024-01-30 20:07:57 +00:00
title => "travelynx: $month_name $year",
2019-03-27 20:20:59 +00:00
journeys => [@journeys],
year => $year,
2019-03-27 20:46:52 +00:00
month => $month,
month_name => $month_name,
2019-03-27 20:20:59 +00:00
statistics => $stats
}
);
}
2019-03-22 15:56:49 +00:00
sub journey_details {
my ($self) = @_;
my $journey_id = $self->stash('id');
2023-02-15 19:01:43 +00:00
my $user = $self->current_user;
my $uid = $user->{id};
2019-03-22 15:56:49 +00:00
$self->param( journey_id => $journey_id );
if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) {
2019-03-22 15:56:49 +00:00
$self->render(
'journey',
2020-01-19 18:21:14 +00:00
status => 404,
2019-03-22 15:56:49 +00:00
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,
2019-03-22 15:56:49 +00:00
);
if ($journey) {
my $map_data = $self->journeys_to_map_data(
journeys => [$journey],
include_manual => 1,
);
my $with_share;
2023-02-15 19:01:43 +00:00
my $share_text;
my $visibility
= $self->compute_effective_visibility(
$user->{default_visibility_str},
$journey->{visibility_str} );
if ( $visibility eq 'public'
or $visibility eq 'travelynx'
or $visibility eq 'followers'
or $visibility eq 'unlisted' )
{
2023-02-15 19:01:43 +00:00
my $delay = 'pünktlich ';
if ( $journey->{rt_arrival} != $journey->{sched_arrival} ) {
$delay = sprintf(
'mit %+d ',
(
$journey->{rt_arrival}->epoch
- $journey->{sched_arrival}->epoch
) / 60
);
}
$with_share = 1;
2023-02-15 19:01:43 +00:00
$share_text
= $journey->{km_route}
? sprintf( '%.0f km', $journey->{km_route} )
: 'Fahrt';
$share_text .= sprintf( ' mit %s %s Ankunft %sum %s',
$journey->{type}, $journey->{no},
$delay, $journey->{rt_arrival}->strftime('%H:%M') );
}
$self->render(
'journey',
2024-01-30 20:07:57 +00:00
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 {
2019-03-22 15:56:49 +00:00
$self->render(
'journey',
2020-01-19 18:21:14 +00:00
status => 404,
2019-03-22 15:56:49 +00:00
error => 'notfound',
journey => {}
);
}
}
sub visibility_form {
my ($self) = @_;
my $dep_ts = $self->param('dep_ts');
my $journey_id = $self->param('id');
my $action = $self->param('action') // 'none';
my $user = $self->current_user;
my $user_level = $user->{default_visibility_str};
my $uid = $user->{id};
my $status = $self->get_user_status;
2023-03-02 20:20:59 +00:00
my $visibility = $status->{visibility_str};
my $journey;
if ($journey_id) {
$journey = $self->journeys->get_single(
uid => $uid,
journey_id => $journey_id,
with_datetime => 1,
with_visibility => 1,
);
2023-03-02 20:20:59 +00:00
$visibility = $journey->{visibility_str};
}
if ( $action eq 'save' ) {
if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
'bad_request',
csrf => 1,
status => 400
);
}
elsif ( $dep_ts and $dep_ts != $status->{sched_departure}->epoch ) {
$self->render(
'edit_visibility',
error => 'old',
user_level => $user_level,
journey => {}
);
}
else {
if ($dep_ts) {
$self->in_transit->update_visibility(
uid => $uid,
visibility => $self->param('status_level'),
);
$self->redirect_to('/');
$self->run_hook( $uid, 'update' );
}
elsif ($journey_id) {
$self->journeys->update_visibility(
uid => $uid,
id => $journey_id,
visibility => $self->param('status_level'),
);
$self->redirect_to( '/journey/' . $journey_id );
}
}
return;
}
2023-03-02 20:20:59 +00:00
$self->param( status_level => $visibility );
if ($journey_id) {
$self->render(
'edit_visibility',
error => undef,
user_level => $user_level,
journey => $journey
);
}
elsif ( $status->{checked_in} ) {
$self->param( dep_ts => $status->{sched_departure}->epoch );
$self->render(
'edit_visibility',
error => undef,
user_level => $user_level,
journey => $status
);
}
else {
$self->render(
'edit_visibility',
error => 'notfound',
user_level => $user_level,
journey => {}
);
}
}
sub comment_form {
my ($self) = @_;
my $dep_ts = $self->param('dep_ts');
my $status = $self->get_user_status;
if ( not $status->{checked_in} ) {
$self->render(
'edit_comment',
error => 'notfound',
journey => {}
);
}
elsif ( not $dep_ts ) {
$self->param( dep_ts => $status->{sched_departure}->epoch );
$self->param( comment => $status->{comment} );
$self->render(
'edit_comment',
error => undef,
journey => $status
);
}
elsif ( $self->validation->csrf_protect->has_error('csrf_token') ) {
$self->render(
'edit_comment',
error => undef,
journey => $status
);
}
elsif ( $dep_ts != $status->{sched_departure}->epoch ) {
# TODO find and update appropriate past journey (if it exists)
$self->param( comment => $status->{comment} );
$self->render(
'edit_comment',
error => undef,
journey => $status
);
}
else {
$self->app->log->debug("set comment");
my $uid = $self->current_user->{id};
$self->in_transit->update_user_data(
uid => $uid,
user_data => { comment => $self->param('comment') }
);
$self->redirect_to('/');
$self->run_hook( $uid, 'update' );
}
}
sub edit_journey {
my ($self) = @_;
my $journey_id = $self->param('journey_id');
my $uid = $self->current_user->{id};
if ( not( $journey_id =~ m{ ^ \d+ $ }x ) ) {
$self->render(
'edit_journey',
2020-01-19 18:21:14 +00:00
status => 404,
error => 'notfound',
journey => {}
);
return;
}
my $journey = $self->journeys->get_single(
uid => $uid,
journey_id => $journey_id,
verbose => 1,
with_datetime => 1,
);
if ( not $journey ) {
$self->render(
'edit_journey',
2020-01-19 18:21:14 +00:00
status => 404,
error => 'notfound',
journey => {}
);
return;
}
my $error = undef;
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 $db = $self->pg->db;
my $tx = $db->begin;
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival))
{
my $datetime = $parser->parse_datetime( $self->param($key) );
if ( $datetime and $datetime->epoch ne $journey->{$key}->epoch ) {
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
$key => $datetime
);
if ($error) {
last;
}
}
}
for my $key (qw(from_name to_name)) {
if ( defined $self->param($key)
and $self->param($key) ne $journey->{$key} )
{
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
$key => $self->param($key)
);
if ($error) {
last;
}
}
}
2019-08-23 10:02:22 +00:00
for my $key (qw(comment)) {
if (
defined $self->param($key)
and ( not $journey->{user_data}
or $journey->{user_data}{$key} ne $self->param($key) )
)
{
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
$key => $self->param($key)
);
2019-08-23 10:02:22 +00:00
if ($error) {
last;
}
}
}
if ( defined $self->param('route') ) {
my @route_old = map { $_->[0] } @{ $journey->{route} };
my @route_new = split( qr{\r?\n\r?}, $self->param('route') );
@route_new = grep { $_ ne '' } @route_new;
if ( join( '|', @route_old ) ne join( '|', @route_new ) ) {
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
route => [@route_new]
);
}
}
{
my $cancelled_old = $journey->{cancelled} // 0;
my $cancelled_new = $self->param('cancelled') // 0;
if ( $cancelled_old != $cancelled_new ) {
$error = $self->journeys->update(
uid => $uid,
db => $db,
id => $journey->{id},
cancelled => $cancelled_new
);
}
}
if ( not $error ) {
$journey = $self->journeys->get_single(
uid => $uid,
db => $db,
journey_id => $journey_id,
verbose => 1,
with_datetime => 1,
);
$error = $self->journeys->sanity_check($journey);
}
if ( not $error ) {
$tx->commit;
$self->redirect_to("/journey/${journey_id}");
return;
}
}
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') );
}
}
$self->param(
route => join( "\n", map { $_->[0] } @{ $journey->{route} } ) );
$self->param( cancelled => $journey->{cancelled} ? 1 : 0 );
$self->param( from_name => $journey->{from_name} );
$self->param( to_name => $journey->{to_name} );
2019-08-23 10:02:22 +00:00
for my $key (qw(comment)) {
if ( $journey->{user_data} and $journey->{user_data}{$key} ) {
$self->param( $key => $journey->{user_data}{$key} );
}
}
$self->render(
'edit_journey',
with_autocomplete => 1,
error => $error,
journey => $journey
);
}
sub add_journey_form {
my ($self) = @_;
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 @parts = split( qr{\s+}, $self->param('train') );
if ( @parts == 2 ) {
@opt{ 'train_type', 'train_no' } = @parts;
}
elsif ( @parts == 3 ) {
@opt{ 'train_type', 'train_line', 'train_no' } = @parts;
}
else {
$self->render(
'add_journey',
with_autocomplete => 1,
status => 400,
error =>
'Zug muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
);
return;
}
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival))
{
2019-04-26 17:53:01 +00:00
if ( $self->param($key) ) {
my $datetime = $parser->parse_datetime( $self->param($key) );
if ( not $datetime ) {
$self->render(
'add_journey',
with_autocomplete => 1,
status => 400,
2019-04-26 17:53:01 +00:00
error => "${key}: Ungültiges Datums-/Zeitformat"
);
return;
}
$opt{$key} = $datetime;
}
}
$opt{rt_departure} //= $opt{sched_departure};
$opt{rt_arrival} //= $opt{sched_arrival};
for my $key (qw(dep_station arr_station route cancelled comment)) {
$opt{$key} = $self->param($key);
}
if ( $opt{route} ) {
$opt{route} = [ split( qr{\r?\n\r?}, $opt{route} ) ];
}
2019-04-26 17:53:01 +00:00
my $db = $self->pg->db;
my $tx = $db->begin;
$opt{db} = $db;
$opt{uid} = $self->current_user->{id};
2019-04-26 17:53:01 +00:00
my ( $journey_id, $error ) = $self->journeys->add(%opt);
2019-04-26 17:53:01 +00:00
if ( not $error ) {
my $journey = $self->journeys->get_single(
2019-04-26 17:53:01 +00:00
uid => $self->current_user->{id},
db => $db,
journey_id => $journey_id,
verbose => 1
);
$error = $self->journeys->sanity_check($journey);
2019-04-26 17:53:01 +00:00
}
if ($error) {
$self->render(
'add_journey',
with_autocomplete => 1,
status => 400,
2019-04-26 17:53:01 +00:00
error => $error,
);
}
else {
$tx->commit;
$self->redirect_to("/journey/${journey_id}");
}
}
else {
$self->render(
'add_journey',
with_autocomplete => 1,
2019-04-26 17:53:01 +00:00
error => undef
);
}
}
1;