summaryrefslogtreecommitdiff
path: root/lib/Travelynx/Controller/Traveling.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx/Controller/Traveling.pm')
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm1998
1 files changed, 1658 insertions, 340 deletions
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index 56de0fb..89385e1 100755
--- a/lib/Travelynx/Controller/Traveling.pm
+++ b/lib/Travelynx/Controller/Traveling.pm
@@ -1,108 +1,465 @@
package Travelynx::Controller::Traveling;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
use DateTime;
use DateTime::Format::Strptime;
-use List::Util qw(uniq);
-use List::UtilsBy qw(uniq_by);
+use List::Util qw(uniq min max);
+use List::UtilsBy qw(max_by uniq_by);
use List::MoreUtils qw(first_index);
+use Mojo::Promise;
+use Text::CSV;
use Travel::Status::DE::IRIS::Stations;
-sub homepage {
- my ($self) = @_;
- if ( $self->is_user_authenticated ) {
- $self->render(
- 'landingpage',
- version => $self->app->config->{version} // 'UNKNOWN',
- with_autocomplete => 1,
- with_geolocation => 1
- );
- $self->mark_seen( $self->current_user->{id} );
- }
- else {
- $self->render(
- 'landingpage',
- version => $self->app->config->{version} // 'UNKNOWN',
- intro => 1
- );
+# Internal Helpers
+
+sub has_str_in_list {
+ my ( $str, @strs ) = @_;
+ if ( List::Util::any { $str eq $_ } @strs ) {
+ return 1;
}
+ return;
}
-sub user_status {
- my ($self) = @_;
+sub get_connecting_trains_p {
+ my ( $self, %opt ) = @_;
- my $name = $self->stash('name');
- my $ts = $self->stash('ts');
- my $user = $self->get_privacy_by_name($name);
+ my $uid = $opt{uid} //= $self->current_user->{id};
+ my $use_history = $self->users->use_history( uid => $uid );
- if ( $user and ( $user->{public_level} & 0x02 ) ) {
- my $status = $self->get_user_status( $user->{id} );
+ my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );
+ my $now = $self->now->epoch;
+ my ( $stationinfo, $arr_epoch, $arr_platform, $arr_countdown );
- my %tw_data = (
- card => 'summary',
- site => '@derfnull',
- image => $self->url_for('/static/icons/icon-512x512.png')
- ->to_abs->scheme('https'),
- );
+ my $promise = Mojo::Promise->new;
- if (
- $ts
- and ( not $status->{checked_in}
- or $status->{sched_departure}->epoch != $ts )
- )
- {
- $tw_data{title} = "Bahnfahrt beendet";
- $tw_data{description} = "${name} hat das Ziel erreicht";
- }
- elsif ( $status->{checked_in} ) {
- $tw_data{title} = "${name} ist unterwegs";
- $tw_data{description} = sprintf(
- '%s %s von %s nach %s',
- $status->{train_type},
- $status->{train_line} // $status->{train_no},
- $status->{dep_name},
- $status->{arr_name} // 'irgendwo'
- );
- if ( $status->{real_arrival}->epoch ) {
- $tw_data{description} .= $status->{real_arrival}
- ->strftime(' – Ankunft gegen %H:%M Uhr');
- }
+ if ( $opt{eva} ) {
+ if ( $use_history & 0x01 ) {
+ $eva = $opt{eva};
}
- else {
- $tw_data{title} = "${name} ist gerade nicht eingecheckt";
- $tw_data{description} = "Letztes Fahrtziel: $status->{arr_name}";
+ 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};
+ }
}
+ }
- $self->render(
- 'user_status',
- name => $name,
- journey => $status,
- twitter => \%tw_data,
- );
+ $exclude_before //= $now - 300;
+
+ if ( not $eva ) {
+ 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 ) {
+ 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;
+ }
+
+ @{ $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;
+ }
+
+ # 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.
+
+ 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;
+ }
+ }
+ }
+ 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 ] );
+
+ # 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;
+ }
+ }
+ }
+ }
+
+ @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;
+ }
+
+ # 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}{"*"};
+ }
+ 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';
+ }
+ }
+ }
+
+ $iris_promise->resolve( [ @results, @cancellations ] );
+ return;
+ }
+ )->catch(
+ sub {
+ $iris_promise->resolve( [] );
+ return;
+ }
+ )->wait;
}
else {
- $self->render('not_found');
+ $iris_promise->resolve( [] );
}
-}
-sub public_status_card {
- my ($self) = @_;
+ 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;
+ }
+ }
+ }
+ for my $hafas_train (@all_hafas_trains) {
+ if ( $hafas_train->{iris_seen} ) {
+ next;
+ }
+ if ( $hafas_train->station_eva >= 8000000 ) {
+
+ # better safe than sorry, for now
+ next;
+ }
+ for my $stop ( $hafas_train->route ) {
+ for my $dest (@destinations) {
+ if ( $stop->loc->name
+ 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: $@"
+ );
+ }
+
+ $promise->resolve( \@iris_trains, \@hafas_trains );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
- my $name = $self->stash('name');
- my $user = $self->get_privacy_by_name($name);
+ return $promise;
+}
- delete $self->stash->{layout};
+sub compute_effective_visibility {
+ my ( $self, $default_visibility, $journey_visibility ) = @_;
+ if ( $journey_visibility eq 'default' ) {
+ return $default_visibility;
+ }
+ return $journey_visibility;
+}
- if ( $user and ( $user->{public_level} & 0x02 ) ) {
- my $status = $self->get_user_status( $user->{id} );
+# Controllers
+
+sub homepage {
+ my ($self) = @_;
+ if ( $self->is_user_authenticated ) {
+ 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 ) )
+ {
+ $self->render_later;
+ $self->get_connecting_trains_p->then(
+ sub {
+ my ( $connections_iris, $connections_hafas ) = @_;
+ $self->render(
+ 'landingpage',
+ user_status => $status,
+ journey_visibility => $journey_visibility,
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
+ );
+ $self->users->mark_seen( uid => $uid );
+ }
+ )->catch(
+ sub {
+ $self->render(
+ 'landingpage',
+ user_status => $status,
+ journey_visibility => $journey_visibility,
+ );
+ $self->users->mark_seen( uid => $uid );
+ }
+ )->wait;
+ return;
+ }
+ else {
+ $self->render(
+ 'landingpage',
+ user_status => $status,
+ journey_visibility => $journey_visibility,
+ );
+ $self->users->mark_seen( uid => $uid );
+ return;
+ }
+ }
+ else {
+ @recent_targets = uniq_by { $_->{eva} }
+ $self->journeys->get_latest_checkout_stations( uid => $uid );
+ }
$self->render(
- '_public_status_card',
- name => $name,
- journey => $status
+ 'landingpage',
+ user_status => $status,
+ recent_targets => \@recent_targets,
+ with_autocomplete => 1,
+ with_geolocation => 1
);
+ $self->users->mark_seen( uid => $uid );
}
else {
- $self->render('not_found');
+ $self->render( 'landingpage', intro => 1 );
}
}
@@ -112,10 +469,93 @@ sub status_card {
delete $self->stash->{layout};
+ my @timeline = $self->in_transit->get_timeline(
+ uid => $self->current_user->{id},
+ short => 1
+ );
+ $self->stash( timeline => [@timeline] );
+
if ( $status->{checked_in} ) {
- $self->render( '_checked_in', journey => $status );
+ 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 ) )
+ {
+ $self->render_later;
+ $self->get_connecting_trains_p->then(
+ sub {
+ my ( $connections_iris, $connections_hafas ) = @_;
+ $self->render(
+ '_checked_in',
+ journey => $status,
+ journey_visibility => $journey_visibility,
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
+ );
+ }
+ )->catch(
+ sub {
+ $self->render(
+ '_checked_in',
+ journey => $status,
+ journey_visibility => $journey_visibility,
+ );
+ }
+ )->wait;
+ return;
+ }
+ $self->render(
+ '_checked_in',
+ journey => $status,
+ journey_visibility => $journey_visibility,
+ );
+ }
+ elsif ( $status->{cancellation} ) {
+ $self->render_later;
+ $self->get_connecting_trains_p(
+ eva => $status->{cancellation}{dep_eva},
+ destination_name => $status->{cancellation}{arr_name}
+ )->then(
+ sub {
+ my ($connecting_trains) = @_;
+ $self->render(
+ '_cancelled_departure',
+ journey => $status->{cancellation},
+ connections_iris => $connecting_trains
+ );
+ }
+ )->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 ) ) {
+ $self->render_later;
+ $self->get_connecting_trains_p->then(
+ sub {
+ my ( $connections_iris, $connections_hafas ) = @_;
+ $self->render(
+ '_checked_out',
+ journey => $status,
+ connections_iris => $connections_iris,
+ connections_hafas => $connections_hafas,
+ );
+ }
+ )->catch(
+ sub {
+ $self->render( '_checked_out', journey => $status );
+ }
+ )->wait;
+ return;
+ }
$self->render( '_checked_out', journey => $status );
}
}
@@ -128,38 +568,71 @@ sub geolocation {
if ( not $lon or not $lat ) {
$self->render( json => { error => 'Invalid lon/lat received' } );
+ return;
}
- else {
- my @candidates = map {
- {
- ds100 => $_->[0][0],
- name => $_->[0][1],
- eva => $_->[0][2],
- lon => $_->[0][3],
- lat => $_->[0][4],
- distance => $_->[1],
- }
- } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon,
- $lat, 10 );
- @candidates = uniq_by { $_->{name} } @candidates;
- if ( @candidates > 5 ) {
+ $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;
+ if ( @hafas > 10 ) {
+ @hafas = @hafas[ 0 .. 9 ];
+ }
+ my @results = map { $_->[0] }
+ sort { $a->[1] <=> $b->[1] }
+ map { [ $_, $_->{distance} ] } ( @iris, @hafas );
$self->render(
json => {
- candidates => [ @candidates[ 0 .. 4 ] ],
+ candidates => [@results],
}
);
}
- else {
+ )->catch(
+ sub {
+ my ($err) = @_;
$self->render(
json => {
- candidates => [@candidates],
+ candidates => [@iris],
+ warning => $err,
}
);
}
- }
+ )->wait;
}
-sub log_action {
+sub travel_action {
my ($self) = @_;
my $params = $self->req->json;
@@ -194,61 +667,124 @@ sub log_action {
if ( $params->{action} eq 'checkin' ) {
- my ( $train, $error )
- = $self->checkin( $params->{station}, $params->{train} );
- my $destination = $params->{dest};
+ my $status = $self->get_user_status;
+ my $promise;
- if ($error) {
- $self->render(
- json => {
- success => 0,
- error => $error,
- },
- );
- }
- elsif ( not $destination ) {
- $self->render(
- json => {
- success => 1,
- redirect_to => '/',
- },
- );
+ if ( $status->{checked_in}
+ and $status->{arr_eva}
+ and $status->{arrival_countdown} <= 0 )
+ {
+ $promise = $self->checkout_p( station => $status->{arr_eva} );
}
else {
- # Silently ignore errors -- if they are permanent, the user will see
- # them when selecting the destination manually.
- my ( $still_checked_in, undef )
- = $self->checkout( $destination, 0 );
- my $station_link = '/s/' . $destination;
- $self->render(
- json => {
- success => 1,
- redirect_to => $still_checked_in ? '/' : $station_link,
- },
- );
+ $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;
+ }
+
+ # 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' ) {
- my ( $still_checked_in, $error )
- = $self->checkout( $params->{station}, $params->{force} );
- my $station_link = '/s/' . $params->{station};
+ $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,
- },
- );
- }
+ 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;
@@ -264,7 +800,12 @@ sub log_action {
else {
my $redir = '/';
if ( $status->{checked_in} or $status->{cancelled} ) {
- $redir = '/s/' . $status->{dep_ds100};
+ if ( $status->{train_id} =~ m{[|]} ) {
+ $redir = '/s/' . $status->{dep_eva} . '?hafas=1';
+ }
+ else {
+ $redir = '/s/' . $status->{dep_ds100};
+ }
}
$self->render(
json => {
@@ -275,50 +816,77 @@ sub log_action {
}
}
elsif ( $params->{action} eq 'cancelled_from' ) {
- my ( undef, $error )
- = $self->checkin( $params->{station}, $params->{train} );
-
- if ($error) {
- $self->render(
- json => {
- success => 0,
- error => $error,
- },
- );
- }
- else {
- $self->render(
- json => {
- success => 1,
- redirect_to => '/',
- },
- );
- }
+ $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' ) {
- my ( undef, $error )
- = $self->checkout( $params->{station}, 1 );
-
- if ($error) {
- $self->render(
- json => {
- success => 0,
- error => $error,
- },
- );
- }
- else {
- $self->render(
- json => {
- success => 1,
- redirect_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;
}
elsif ( $params->{action} eq 'delete' ) {
- my $error = $self->delete_journey( $params->{id}, $params->{checkin},
- $params->{checkout} );
+ my $error = $self->journeys->delete(
+ uid => $self->current_user->{id},
+ id => $params->{id},
+ checkin => $params->{checkin},
+ checkout => $params->{checkout}
+ );
if ($error) {
$self->render(
json => {
@@ -347,57 +915,277 @@ sub log_action {
}
sub station {
- my ($self) = @_;
- my $station = $self->stash('station');
- my $train = $self->param('train');
+ 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 );
- my $status = $self->get_departures( $station, 120, 30, 1 );
+ $self->render_later;
- if ( $status->{errstr} ) {
- $self->render(
- 'landingpage',
- version => $self->app->config->{version} // 'UNKNOWN',
- with_autocomplete => 1,
- with_geolocation => 1,
- error => $status->{errstr}
+ if ( $timestamp and $timestamp =~ m{ ^ \d+ $ }x ) {
+ $timestamp = DateTime->from_epoch(
+ epoch => $timestamp,
+ time_zone => 'Europe/Berlin'
);
}
else {
- # You can't check into a train which terminates here
- my @results = grep { $_->departure } @{ $status->{results} };
-
- @results = map { $_->[0] }
- sort { $b->[1] <=> $a->[1] }
- map { [ $_, $_->departure->epoch // $_->sched_departure->epoch ] }
- @results;
-
- if ($train) {
- @results
- = grep { $_->type . ' ' . $_->train_no eq $train } @results;
- }
+ $timestamp = DateTime->now( time_zone => 'Europe/Berlin' );
+ }
- $self->render(
- 'departures',
- ds100 => $status->{station_ds100},
- results => \@results,
- station => $status->{station_name},
- related_stations => $status->{related_stations},
- title => "travelynx: $status->{station_name}",
+ my $use_hafas = $self->param('hafas');
+ my $promise;
+ if ($use_hafas) {
+ $promise = $self->hafas->get_departures_p(
+ eva => $station,
+ timestamp => $timestamp,
+ lookbehind => 30,
+ lookahead => 30,
);
}
- $self->mark_seen( $self->current_user->{id} );
+ else {
+ $promise = $self->iris->get_departures_p(
+ station => $station,
+ lookbehind => 120,
+ lookahead => 30,
+ with_related => 1,
+ );
+ }
+ $promise->then(
+ sub {
+ my ($status) = @_;
+ my $api_link;
+ my @results;
+
+ my $now = $self->now->epoch;
+ my $now_within_range
+ = abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0;
+
+ if ($use_hafas) {
+
+ 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 { [ $_, $_->datetime->epoch ] } $status->results;
+ $self->stations->add_meta(
+ eva => $status->station->{eva},
+ meta => $status->station->{evas} // []
+ );
+ $status = {
+ station_eva => $status->station->{eva},
+ station_name => (
+ List::Util::reduce { length($a) < length($b) ? $a : $b }
+ @{ $status->station->{names} }
+ ),
+ related_stations => [],
+ };
+ }
+ else {
+
+ $api_link = '/s/' . $status->{station_eva} . '?hafas=1';
+
+ # You can't check into a train which terminates here
+ @results = grep { $_->departure } @{ $status->{results} };
+
+ @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;
+ }
+ }
+ }
+
+ my $connections_p;
+ if ( $trip_id and $use_hafas ) {
+ @results = grep { $_->id eq $trip_id } @results;
+ }
+ elsif ( $train and not $use_hafas ) {
+ @results
+ = grep { $_->type . ' ' . $_->train_no eq $train } @results;
+ }
+ else {
+ if ( $user_status->{cancellation}
+ and $status->{station_eva} eq
+ $user_status->{cancellation}{dep_eva} )
+ {
+ $connections_p = $self->get_connecting_trains_p(
+ eva => $user_status->{cancellation}{dep_eva},
+ destination_name =>
+ $user_status->{cancellation}{arr_name}
+ );
+ }
+ else {
+ $connections_p = $self->get_connecting_trains_p(
+ eva => $status->{station_eva} );
+ }
+ }
+
+ if ($connections_p) {
+ $connections_p->then(
+ sub {
+ my ( $connections_iris, $connections_hafas ) = @_;
+ $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,
+ title => "travelynx: $status->{station_name}",
+ );
+ }
+ )->catch(
+ sub {
+ $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,
+ api_link => $api_link,
+ title => "travelynx: $status->{station_name}",
+ );
+ }
+ )->wait;
+ }
+ else {
+ $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,
+ api_link => $api_link,
+ title => "travelynx: $status->{station_name}",
+ );
+ }
+ }
+ )->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
+ );
+ }
+ }
+ )->wait;
+ $self->users->mark_seen( uid => $uid );
}
sub redirect_to_station {
my ($self) = @_;
my $station = $self->param('station');
- $self->redirect_to("/s/${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}");
+ }
+ }
+ else {
+ $self->redirect_to("/s/${station}?hafas=1");
+ }
}
sub cancelled {
my ($self) = @_;
- my @journeys = $self->get_user_travels(
+ my @journeys = $self->journeys->get(
+ uid => $self->current_user->{id},
cancelled => 1,
with_datetime => 1
);
@@ -414,7 +1202,122 @@ sub cancelled {
sub history {
my ($self) = @_;
- $self->render( template => 'history' );
+ $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 {
+ # 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,
+ title => 'travelynx: Reisen nach Station',
+ months => [
+ qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
+ ],
+ );
}
sub map_history {
@@ -422,116 +1325,278 @@ sub map_history {
my $location = $self->app->coordinates_by_station;
- my @journeys = $self->get_user_travels;
+ 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');
+ my $with_polyline = $route_type eq 'beeline' ? 0 : 1;
+
+ my $parser = DateTime::Format::Strptime->new(
+ pattern => '%d.%m.%Y',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
+
+ if ( $filter_from
+ and $filter_from =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x )
+ {
+ $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)->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,
+ after => $filter_from,
+ before => $filter_until,
+ );
+
+ if ($filter_type) {
+ my @filter = split( qr{, *}, $filter_type );
+ @journeys
+ = grep { has_str_in_list( $_->{type}, @filter ) } @journeys;
+ }
if ( not @journeys ) {
$self->render(
template => 'history_map',
with_map => 1,
+ skipped_journeys => [],
station_coordinates => [],
- station_pairs => [],
+ polyline_groups => [],
);
return;
}
- my $first_departure = $journeys[-1]->{rt_departure};
- my $last_departure = $journeys[0]->{rt_departure};
+ my $include_manual = $self->param('include_manual') ? 1 : 0;
- my @stations = uniq map { $_->{to_name} } @journeys;
- push( @stations, uniq map { $_->{from_name} } @journeys );
- @stations = uniq @stations;
- my @station_coordinates = map { [ $location->{$_}, $_ ] }
- grep { exists $location->{$_} } @stations;
+ my $res = $self->journeys_to_map_data(
+ journeys => \@journeys,
+ route_type => $route_type,
+ include_manual => $include_manual
+ );
- my @station_pairs;
- my %seen;
+ $self->render(
+ template => 'history_map',
+ year => $year,
+ with_map => 1,
+ title => 'travelynx: Karte',
+ %{$res}
+ );
+}
+
+sub json_history {
+ my ($self) = @_;
+
+ $self->render(
+ json => [ $self->journeys->get( uid => $self->current_user->{id} ) ] );
+}
- for my $journey (@journeys) {
+sub csv_history {
+ my ($self) = @_;
- my @route = map { $_->[0] } @{ $journey->{route} };
- my $from_index = first_index { $_ eq $journey->{from_name} } @route;
- my $to_index = first_index { $_ eq $journey->{to_name} } @route;
+ 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;
- if ( $from_index == -1
- or $to_index == -1
- or $journey->{edited} == 0x3fff )
+ for my $journey (
+ $self->journeys->get(
+ uid => $self->current_user->{id},
+ with_datetime => 1
+ )
+ )
+ {
+ 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}
+ )
+ )
{
- next;
+ $buf .= $csv->string;
}
+ }
- @route = @route[ $from_index .. $to_index ];
-
- my $key = join( '|', @route );
+ $self->render(
+ text => $buf,
+ format => 'csv'
+ );
+}
- if ( $seen{$key} ) {
- next;
- }
+sub year_in_review {
+ my ($self) = @_;
+ my $year = $self->stash('year');
+ my @journeys;
- $seen{$key} = 1;
+ # 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;
+ }
- # direction does not matter at the moment
- $seen{ join( '|', reverse @route ) } = 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 );
+ @journeys = $self->journeys->get(
+ uid => $self->current_user->{id},
+ after => $interval_start,
+ before => $interval_end,
+ with_datetime => 1
+ );
- my $prev_station = shift @route;
- for my $station (@route) {
- push( @station_pairs, [ $prev_station, $station ] );
- $prev_station = $station;
- }
+ if ( not @journeys ) {
+ $self->render(
+ 'not_found',
+ message => 'Keine Zugfahrten im angefragten Jahr gefunden.',
+ status => 404
+ );
+ return;
}
- @station_pairs = uniq_by { $_->[0] . '|' . $_->[1] } @station_pairs;
- @station_pairs
- = grep { exists $location->{ $_->[0] } and exists $location->{ $_->[1] } }
- @station_pairs;
- @station_pairs
- = map { [ $location->{ $_->[0] }, $location->{ $_->[1] } ] }
- @station_pairs;
+ my $now = $self->now;
+ if (
+ not( $year < $now->year or ( $now->month == 12 and $now->day == 31 ) ) )
+ {
+ $self->render(
+ 'not_found',
+ message =>
+'Der aktuelle Jahresrückblick wird erst zum Jahresende (am 31.12.) freigeschaltet',
+ status => 404
+ );
+ return;
+ }
- my @routes;
+ my ( $stats, $review ) = $self->journeys->get_stats(
+ uid => $self->current_user->{id},
+ year => $year,
+ review => 1
+ );
$self->render(
- template => 'history_map',
- with_map => 1,
- station_coordinates => \@station_coordinates,
- station_pairs => \@station_pairs,
+ 'year_in_review',
+ title => "travelynx: Jahresrückblick $year",
+ year => $year,
+ stats => $stats,
+ review => $review,
);
-}
-
-sub json_history {
- my ($self) = @_;
- $self->render( json => [ $self->get_user_travels ] );
}
sub yearly_history {
my ($self) = @_;
- my $year = $self->stash('year');
+ my $year = $self->stash('year');
+ my $filter = $self->param('filter');
my @journeys;
- my $stats;
# 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 ) )
{
- @journeys = $self->get_user_travels( with_datetime => 1 );
+ $self->render( 'not_found', status => 404 );
+ return;
}
- else {
- 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->get_user_travels(
- after => $interval_start,
- before => $interval_end,
- with_datetime => 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 );
+ @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.'
);
- $stats = $self->get_journey_stats( year => $year );
+ return;
+ }
+
+ my $stats = $self->journeys->get_stats(
+ uid => $self->current_user->{id},
+ year => $year
+ );
+
+ 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(
@@ -542,10 +1607,12 @@ sub yearly_history {
}
},
any => {
- template => 'history_by_year',
- journeys => [@journeys],
- year => $year,
- statistics => $stats
+ template => 'history_by_year',
+ title => "travelynx: $year",
+ journeys => [@journeys],
+ year => $year,
+ have_review => $with_review,
+ statistics => $stats
}
);
@@ -556,7 +1623,6 @@ sub monthly_history {
my $year = $self->stash('year');
my $month = $self->stash('month');
my @journeys;
- my $stats;
my @months
= (
qw(Januar Februar März April Mai Juni Juli August September Oktober November Dezember)
@@ -571,30 +1637,43 @@ sub monthly_history {
and $month < 13 )
)
{
- @journeys = $self->get_user_travels( with_datetime => 1 );
+ $self->render( 'not_found', status => 404 );
+ return;
}
- else {
- 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->get_user_travels(
- after => $interval_start,
- before => $interval_end,
- with_datetime => 1
- );
- $stats = $self->get_journey_stats(
- year => $year,
- month => $month
+ 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;
}
+ my $stats = $self->journeys->get_stats(
+ uid => $self->current_user->{id},
+ year => $year,
+ month => $month
+ );
+
+ my $month_name = $months[ $month - 1 ];
+
$self->respond_to(
json => {
json => {
@@ -604,10 +1683,11 @@ sub monthly_history {
},
any => {
template => 'history_by_month',
+ title => "travelynx: $month_name $year",
journeys => [@journeys],
year => $year,
month => $month,
- month_name => $months[ $month - 1 ],
+ month_name => $month_name,
statistics => $stats
}
);
@@ -618,36 +1698,89 @@ sub journey_details {
my ($self) = @_;
my $journey_id = $self->stash('id');
- my $uid = $self->current_user->{id};
+ my $user = $self->current_user;
+ my $uid = $user->{id};
$self->param( journey_id => $journey_id );
- if ( not($journey_id) ) {
+ if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) {
$self->render(
'journey',
+ status => 404,
error => 'notfound',
journey => {}
);
return;
}
- my $journey = $self->get_journey(
- uid => $uid,
- journey_id => $journey_id,
- verbose => 1,
- with_datetime => 1,
+ my $journey = $self->journeys->get_single(
+ uid => $uid,
+ journey_id => $journey_id,
+ verbose => 1,
+ with_datetime => 1,
+ with_polyline => 1,
+ with_visibility => 1,
);
if ($journey) {
+ my $map_data = $self->journeys_to_map_data(
+ journeys => [$journey],
+ include_manual => 1,
+ );
+ my $with_share;
+ 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' )
+ {
+ 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;
+ $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',
- error => undef,
- journey => $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 => {}
);
@@ -655,6 +1788,144 @@ sub journey_details {
}
+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;
+ 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,
+ );
+ $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;
+ }
+
+ $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');
@@ -663,21 +1934,24 @@ sub edit_journey {
if ( not( $journey_id =~ m{ ^ \d+ $ }x ) ) {
$self->render(
'edit_journey',
+ status => 404,
error => 'notfound',
journey => {}
);
return;
}
- my $journey = $self->get_journey(
+ my $journey = $self->journeys->get_single(
uid => $uid,
journey_id => $journey_id,
+ verbose => 1,
with_datetime => 1,
);
if ( not $journey ) {
$self->render(
'edit_journey',
+ status => 404,
error => 'notfound',
journey => {}
);
@@ -700,8 +1974,27 @@ sub edit_journey {
{
my $datetime = $parser->parse_datetime( $self->param($key) );
if ( $datetime and $datetime->epoch ne $journey->{$key}->epoch ) {
- $error = $self->update_journey_part( $db, $journey->{id},
- $key, $datetime );
+ $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;
}
@@ -714,8 +2007,12 @@ sub edit_journey {
or $journey->{user_data}{$key} ne $self->param($key) )
)
{
- $error = $self->update_journey_part( $db, $journey->{id}, $key,
- $self->param($key) );
+ $error = $self->journeys->update(
+ uid => $uid,
+ db => $db,
+ id => $journey->{id},
+ $key => $self->param($key)
+ );
if ($error) {
last;
}
@@ -726,21 +2023,36 @@ sub edit_journey {
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->update_journey_part( $db, $journey->{id}, 'route',
- [@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->get_journey(
+ $journey = $self->journeys->get_single(
uid => $uid,
db => $db,
journey_id => $journey_id,
verbose => 1,
with_datetime => 1,
);
- $error = $self->journey_sanity_check($journey);
+ $error = $self->journeys->sanity_check($journey);
}
if ( not $error ) {
$tx->commit;
@@ -759,6 +2071,10 @@ sub edit_journey {
$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} );
+
for my $key (qw(comment)) {
if ( $journey->{user_data} and $journey->{user_data}{$key} ) {
$self->param( $key => $journey->{user_data}{$key} );
@@ -767,8 +2083,9 @@ sub edit_journey {
$self->render(
'edit_journey',
- error => $error,
- journey => $journey
+ with_autocomplete => 1,
+ error => $error,
+ journey => $journey
);
}
@@ -795,7 +2112,7 @@ sub add_journey_form {
$self->render(
'add_journey',
with_autocomplete => 1,
- error =>
+ error =>
'Zug muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
);
return;
@@ -831,18 +2148,19 @@ sub add_journey_form {
my $db = $self->pg->db;
my $tx = $db->begin;
- $opt{db} = $db;
+ $opt{db} = $db;
+ $opt{uid} = $self->current_user->{id};
- my ( $journey_id, $error ) = $self->add_journey(%opt);
+ my ( $journey_id, $error ) = $self->journeys->add(%opt);
if ( not $error ) {
- my $journey = $self->get_journey(
+ my $journey = $self->journeys->get_single(
uid => $self->current_user->{id},
db => $db,
journey_id => $journey_id,
verbose => 1
);
- $error = $self->journey_sanity_check($journey);
+ $error = $self->journeys->sanity_check($journey);
}
if ($error) {