optionally show local transit connections as well

This commit is contained in:
Daniel Friesel 2022-09-24 18:53:04 +02:00
parent 1c7779e94e
commit e54130ad6e
No known key found for this signature in database
GPG key ID: 100D5BFB5166E005
9 changed files with 292 additions and 126 deletions

View file

@ -1083,6 +1083,29 @@ my @migrations = (
} }
); );
}, },
# v25 -> v26
# travelynx 1.24 adds local transit connections and needs to know targets
# for that to work, as local transit does not support checkins yet.
sub {
my ($db) = @_;
$db->query(
qq{
create table localtransit (
user_id integer not null references users (id) primary key,
data jsonb
);
create view user_transit as select
id,
use_history,
localtransit.data as data
from users
left join localtransit on localtransit.user_id = id
;
update schema_version set version = 26;
}
);
},
); );
sub setup_db { sub setup_db {

View file

@ -149,6 +149,7 @@ sub run {
my $transit_res = $db->delete( 'in_transit', { user_id => $uid } ); my $transit_res = $db->delete( 'in_transit', { user_id => $uid } );
my $hooks_res = $db->delete( 'webhooks', { user_id => $uid } ); my $hooks_res = $db->delete( 'webhooks', { user_id => $uid } );
my $trwl_res = $db->delete( 'traewelling', { user_id => $uid } ); my $trwl_res = $db->delete( 'traewelling', { user_id => $uid } );
my $lt_res = $db->delete( 'localtransit', { user_id => $uid } );
my $password_res my $password_res
= $db->delete( 'pending_passwords', { user_id => $uid } ); = $db->delete( 'pending_passwords', { user_id => $uid } );
my $user_res = $db->delete( 'users', { id => $uid } ); my $user_res = $db->delete( 'users', { id => $uid } );

View file

@ -6,7 +6,7 @@ package Travelynx::Controller::Account;
use Mojo::Base 'Mojolicious::Controller'; use Mojo::Base 'Mojolicious::Controller';
use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64); use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
use UUID::Tiny qw(:std); use UUID::Tiny qw(:std);
# Internal Helpers # Internal Helpers
@ -498,8 +498,11 @@ sub privacy {
sub insight { sub insight {
my ($self) = @_; my ($self) = @_;
my $user = $self->current_user; my $user = $self->current_user;
my $use_history = $self->users->use_history( uid => $user->{id} ); my ( $use_history, $destinations ) = $self->users->use_history(
uid => $user->{id},
with_local_transit => 1
);
if ( $self->param('action') and $self->param('action') eq 'save' ) { if ( $self->param('action') and $self->param('action') eq 'save' ) {
if ( $self->param('on_departure') ) { if ( $self->param('on_departure') ) {
@ -516,16 +519,31 @@ sub insight {
$use_history &= ~0x02; $use_history &= ~0x02;
} }
if ( $self->param('local_transit') ) {
$use_history |= 0x04;
}
else {
$use_history &= ~0x04;
}
if ( $self->param('destinations') ) {
$destinations
= [ split( qr{\r?\n\r?}, $self->param('destinations') ) ];
}
$self->users->use_history( $self->users->use_history(
uid => $user->{id}, uid => $user->{id},
set => $use_history set => $use_history,
destinations => $destinations
); );
$self->flash( success => 'use_history' ); $self->flash( success => 'use_history' );
$self->redirect_to('account'); $self->redirect_to('account');
} }
$self->param( on_departure => $use_history & 0x01 ? 1 : 0 ); $self->param( on_departure => $use_history & 0x01 ? 1 : 0 );
$self->param( on_arrival => $use_history & 0x02 ? 1 : 0 ); $self->param( on_arrival => $use_history & 0x02 ? 1 : 0 );
$self->param( local_transit => $use_history & 0x04 ? 1 : 0 );
$self->param( destinations => join( "\n", @{$destinations} ) );
$self->render('use_history'); $self->render('use_history');
} }

View file

@ -28,8 +28,11 @@ sub has_str_in_list {
sub get_connecting_trains_p { sub get_connecting_trains_p {
my ( $self, %opt ) = @_; my ( $self, %opt ) = @_;
my $uid = $opt{uid} //= $self->current_user->{id}; my $uid = $opt{uid} //= $self->current_user->{id};
my $use_history = $self->users->use_history( uid => $uid ); my ( $use_history, $lt_stops ) = $self->users->use_history(
uid => $uid,
with_local_transit => 1
);
my ( $eva, $exclude_via, $exclude_train_id, $exclude_before ); my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );
my $now = $self->now->epoch; my $now = $self->now->epoch;
@ -72,7 +75,7 @@ sub get_connecting_trains_p {
@destinations = grep { $_ ne $exclude_via } @destinations; @destinations = grep { $_ ne $exclude_via } @destinations;
} }
if ( not @destinations ) { if ( not( @destinations or $use_history & 0x04 and @{$lt_stops} ) ) {
return $promise->reject; return $promise->reject;
} }
@ -82,43 +85,44 @@ sub get_connecting_trains_p {
my $iris_promise = Mojo::Promise->new; my $iris_promise = Mojo::Promise->new;
$self->iris->get_departures_p( if (@destinations) {
station => $eva, $self->iris->get_departures_p(
lookbehind => 10, station => $eva,
lookahead => $lookahead, lookbehind => 10,
with_related => 1 lookahead => $lookahead,
)->then( with_related => 1
sub { )->then(
my ($stationboard) = @_; sub {
if ( $stationboard->{errstr} ) { my ($stationboard) = @_;
$iris_promise->reject( $stationboard->{errstr} ); if ( $stationboard->{errstr} ) {
return; $iris_promise->reject( $stationboard->{errstr} );
} return;
}
@{ $stationboard->{results} } = map { $_->[0] } @{ $stationboard->{results} } = map { $_->[0] }
sort { $a->[1] <=> $b->[1] } sort { $a->[1] <=> $b->[1] }
map { [ $_, $_->departure ? $_->departure->epoch : 0 ] } map { [ $_, $_->departure ? $_->departure->epoch : 0 ] }
@{ $stationboard->{results} }; @{ $stationboard->{results} };
my @results; my @results;
my @cancellations; my @cancellations;
my $excluded_train; my $excluded_train;
my %via_count = map { $_ => 0 } @destinations; my %via_count = map { $_ => 0 } @destinations;
for my $train ( @{ $stationboard->{results} } ) { for my $train ( @{ $stationboard->{results} } ) {
if ( not $train->departure ) { if ( not $train->departure ) {
next; next;
} }
if ( $exclude_before if ( $exclude_before
and $train->departure and $train->departure
and $train->departure->epoch < $exclude_before ) and $train->departure->epoch < $exclude_before )
{ {
next; next;
} }
if ( $exclude_train_id if ( $exclude_train_id
and $train->train_id eq $exclude_train_id ) and $train->train_id eq $exclude_train_id )
{ {
$excluded_train = $train; $excluded_train = $train;
next; next;
} }
# In general, this function is meant to return feasible # In general, this function is meant to return feasible
# connections. However, cancelled connections may also be of # connections. However, cancelled connections may also be of
@ -136,97 +140,105 @@ sub get_connecting_trains_p {
# the route. Also note that this specific case is not yet handled # the route. Also note that this specific case is not yet handled
# properly by the cancellation logic etc. # properly by the cancellation logic etc.
if ( $train->departure_is_cancelled ) { if ( $train->departure_is_cancelled ) {
my @via my @via = (
= ( $train->sched_route_post, $train->sched_route_end ); $train->sched_route_post, $train->sched_route_end
for my $dest (@destinations) { );
if ( has_str_in_list( $dest, @via ) ) { for my $dest (@destinations) {
push( @cancellations, [ $train, $dest ] ); if ( has_str_in_list( $dest, @via ) ) {
next; push( @cancellations, [ $train, $dest ] );
next;
}
} }
} }
} else {
else { my @via = ( $train->route_post, $train->route_end );
my @via = ( $train->route_post, $train->route_end ); for my $dest (@destinations) {
for my $dest (@destinations) { if ( $via_count{$dest} < 2
if ( $via_count{$dest} < 2 and has_str_in_list( $dest, @via ) )
and has_str_in_list( $dest, @via ) ) {
{ push( @results, [ $train, $dest ] );
push( @results, [ $train, $dest ] );
# Show all past and up to two future departures per destination # Show all past and up to two future departures per destination
if ( not $train->departure if ( not $train->departure
or $train->departure->epoch >= $now ) or $train->departure->epoch >= $now )
{ {
$via_count{$dest}++; $via_count{$dest}++;
}
next;
} }
next;
} }
} }
} }
}
@results = map { $_->[0] } @results = map { $_->[0] }
sort { $a->[1] <=> $b->[1] } sort { $a->[1] <=> $b->[1] }
map { map {
[ [
$_, $_,
$_->[0]->departure->epoch $_->[0]->departure->epoch
// $_->[0]->sched_departure->epoch // $_->[0]->sched_departure->epoch
] ]
} @results; } @results;
@cancellations = map { $_->[0] } @cancellations = map { $_->[0] }
sort { $a->[1] <=> $b->[1] } sort { $a->[1] <=> $b->[1] }
map { [ $_, $_->[0]->sched_departure->epoch ] } @cancellations; map { [ $_, $_->[0]->sched_departure->epoch ] }
@cancellations;
# remove trains whose route matches the excluded one's # remove trains whose route matches the excluded one's
if ($excluded_train) { if ($excluded_train) {
my $route_pre = join( '|', reverse $excluded_train->route_pre ); my $route_pre
@results = join( '|', reverse $excluded_train->route_pre );
= grep { join( '|', $_->[0]->route_post ) ne $route_pre } @results
@results; = grep { join( '|', $_->[0]->route_post ) ne $route_pre }
my $route_post = join( '|', $excluded_train->route_post ); @results;
@results my $route_post = join( '|', $excluded_train->route_post );
= grep { join( '|', $_->[0]->route_post ) ne $route_post } @results
@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} ) {
$interchange_duration
= $stationinfo->{i}{$arr_platform}{ $train->platform };
$interchange_duration //= $stationinfo->{i}{"*"};
} }
if ( defined $interchange_duration ) {
my $interchange_time # add message IDs and 'transfer short' hints
= ( $train->departure->epoch - $arr_epoch ) / 60; for my $result (@results) {
if ( $interchange_time < $interchange_duration ) { my $train = $result->[0];
$train->{interchange_text} = 'Anschluss knapp'; my @message_ids
$train->{interchange_icon} = 'directions_run'; = List::Util::uniq map { $_->[1] } $train->raw_messages;
$train->{message_id} = { map { $_ => 1 } @message_ids };
my $interchange_duration;
if ( exists $stationinfo->{i} ) {
$interchange_duration
= $stationinfo->{i}{$arr_platform}
{ $train->platform };
$interchange_duration //= $stationinfo->{i}{"*"};
} }
elsif ( $interchange_time == $interchange_duration ) { if ( defined $interchange_duration ) {
$train->{interchange_text} my $interchange_time
= 'Anschluss könnte knapp werden'; = ( $train->departure->epoch - $arr_epoch ) / 60;
$train->{interchange_icon} = 'directions_run'; 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 ] ); $iris_promise->resolve( [ @results, @cancellations ] );
return; return;
} }
)->catch( )->catch(
sub { sub {
$iris_promise->reject(@_); $iris_promise->reject(@_);
return; return;
} }
)->wait; )->wait;
}
else {
$iris_promise->resolve( [] );
}
my $hafas_promise = Mojo::Promise->new; my $hafas_promise = Mojo::Promise->new;
my $rest_api = $self->config->{backend}{hafas_rest_api}; my $rest_api = $self->config->{backend}{hafas_rest_api};
@ -254,6 +266,7 @@ sub get_connecting_trains_p {
my ( $iris, $hafas ) = @_; my ( $iris, $hafas ) = @_;
my @iris_trains = @{ $iris->[0] }; my @iris_trains = @{ $iris->[0] };
my @hafas_trains = @{ $hafas->[0] }; my @hafas_trains = @{ $hafas->[0] };
my @transit_fyi;
my $strp = DateTime::Format::Strptime->new( my $strp = DateTime::Format::Strptime->new(
pattern => '%Y-%m-%dT%H:%M:%S%z', pattern => '%Y-%m-%dT%H:%M:%S%z',
@ -296,6 +309,44 @@ sub get_connecting_trains_p {
} }
} }
} }
if ( $use_history & 0x04 and @{$lt_stops} ) {
my %via_count = map { $_ => 0 } @{$lt_stops};
for my $hafas_train (@hafas_trains) {
for
my $stop ( @{ $hafas_train->{nextStopovers} // [] } )
{
for my $dest ( @{$lt_stops} ) {
if ( $stop->{stop}{name}
and $stop->{stop}{name} eq $dest
and $via_count{$dest} < 2
and $hafas_train->{when} )
{
my $departure = $strp->parse_datetime(
$hafas_train->{when} );
my $arrival
= $strp->parse_datetime(
$stop->{arrival} );
if ( $departure->epoch >= $exclude_before )
{
$via_count{$dest}++;
push(
@transit_fyi,
[
{
line =>
$hafas_train->{line}
{name},
departure => $departure,
},
$dest, $arrival
]
);
}
}
}
}
}
}
}; };
if ($@) { if ($@) {
$self->app->log->error( $self->app->log->error(
@ -303,7 +354,7 @@ sub get_connecting_trains_p {
); );
} }
$promise->resolve( \@iris_trains ); $promise->resolve( \@iris_trains, \@transit_fyi );
return; return;
} }
)->catch( )->catch(

View file

@ -9,6 +9,7 @@ use warnings;
use 5.020; use 5.020;
use DateTime; use DateTime;
use JSON;
my @sb_templates = ( my @sb_templates = (
undef, undef,
@ -483,12 +484,34 @@ sub use_history {
my $uid = $opt{uid}; my $uid = $opt{uid};
my $value = $opt{set}; my $value = $opt{set};
if ( $opt{destinations} ) {
$db->insert(
'localtransit',
{
user_id => $uid,
data =>
JSON->new->encode( { destinations => $opt{destinations} } )
},
{ on_conflict => \'(user_id) do update set data = EXCLUDED.data' }
);
}
if ($value) { if ($value) {
$db->update( 'users', { use_history => $value }, { id => $uid } ); $db->update( 'users', { use_history => $value }, { id => $uid } );
} }
else { else {
return $db->select( 'users', ['use_history'], { id => $uid } ) if ( $opt{with_local_transit} ) {
->hash->{use_history}; my $res = $db->select(
'user_transit',
[ 'use_history', 'data' ],
{ id => $uid }
)->expand->hash;
return ( $res->{use_history}, $res->{data}{destinations} // [] );
}
else {
return $db->select( 'users', ['use_history'], { id => $uid } )
->hash->{use_history};
}
} }
} }

View file

@ -200,6 +200,13 @@
% } % }
%= include '_connections', connections => \@connections, checkin_from => $journey->{arrival_countdown} < 0 ? $journey->{arr_ds100} : undef; %= include '_connections', connections => \@connections, checkin_from => $journey->{arrival_countdown} < 0 ? $journey->{arr_ds100} : undef;
% } % }
% if (my @transit_fyi = @{stash('transit_fyi') // []}) {
<span class="card-title" style="margin-top: 2ex;">Nahverkehr</span>
% if ($journey->{arrival_countdown} < 0) {
<p>Nur zur Information kein Checkin möglich.</p>
% }
%= include '_transit_fyi', transit_fyi => \@transit_fyi;
% }
% if (defined $journey->{arrival_countdown} and $journey->{arrival_countdown} <= 0) { % if (defined $journey->{arrival_countdown} and $journey->{arrival_countdown} <= 0) {
<p style="margin-top: 2ex;"> <p style="margin-top: 2ex;">
Der automatische Checkout erfolgt wegen gelegentlich veralteter Der automatische Checkout erfolgt wegen gelegentlich veralteter

View file

@ -0,0 +1,19 @@
<table class="striped"><tbody>
% for my $res (@{$transit_fyi}) {
% my ($info, $via, $via_arr) = @{$res};
% $via_arr = $via_arr ? $via_arr->strftime('%H:%M') : q{};
<tr>
<td>
%= $info->{line}
<br/>
%= $info->{departure}->strftime('%H:%M')
% if ($info->{departure_delay}) {
%= sprintf('(%+d)', $info->{departure_delay})
% }
</td>
<td>
<%= $via %><br/><%= $via_arr %>
</td>
</tr>
% }
</tbody></table>

View file

@ -61,7 +61,7 @@
<th scope="row">Verbindungen</th> <th scope="row">Verbindungen</th>
<td> <td>
<a href="/account/insight"><i class="material-icons">edit</i></a> <a href="/account/insight"><i class="material-icons">edit</i></a>
% if ($use_history & 0x03) { % if ($use_history & 0x07) {
Vorschläge aktiv Vorschläge aktiv
% } % }
% else { % else {

View file

@ -47,6 +47,30 @@
ohne Umweg über die Abfahrtstafel möglich. ohne Umweg über die Abfahrtstafel möglich.
</div> </div>
</div> </div>
<div class="row">
<div class="input-field col s12">
<label>
%= check_box local_transit => 1
<span>Nahverkehr</span>
</label>
</div>
</div>
<div class="row">
<div class="col s12">
Zeige beim Reisestatus zusätzlich Anschlussmöglichkeiten an den
Nahverkehr. Diese dienen lediglich zur Information; ein Checkin ist
nicht möglich. Es werden nur Anschlussmöglichkeiten zu Zielen
angezeigt, die im folgenden Feld gelistet sind (ein Ziel pro
Zeile, z.B. „Eichlinghofen H-Bahn, Dortmund“). Falls travelynx in
Zukunft eine Möglichkeit für Checkins in Nahverkehrsmittel erhält,
wird diese Liste ggf. gelöscht.
</div>
</div>
<div class="row">
<div class="col s12">
%= text_area 'destinations', id => 'destinations', class => 'materialize-textarea'
</div>
</div>
<div class="row"> <div class="row">
<div class="col s3 m3 l3"> <div class="col s3 m3 l3">
</div> </div>