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 {

View file

@ -149,6 +149,7 @@ sub run {
my $transit_res = $db->delete( 'in_transit', { user_id => $uid } );
my $hooks_res = $db->delete( 'webhooks', { user_id => $uid } );
my $trwl_res = $db->delete( 'traewelling', { user_id => $uid } );
my $lt_res = $db->delete( 'localtransit', { user_id => $uid } );
my $password_res
= $db->delete( 'pending_passwords', { user_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 Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64);
use UUID::Tiny qw(:std);
use UUID::Tiny qw(:std);
# Internal Helpers
@ -498,8 +498,11 @@ sub privacy {
sub insight {
my ($self) = @_;
my $user = $self->current_user;
my $use_history = $self->users->use_history( uid => $user->{id} );
my $user = $self->current_user;
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('on_departure') ) {
@ -516,16 +519,31 @@ sub insight {
$use_history &= ~0x02;
}
if ( $self->param('local_transit') ) {
$use_history |= 0x04;
}
else {
$use_history &= ~0x04;
}
if ( $self->param('destinations') ) {
$destinations
= [ split( qr{\r?\n\r?}, $self->param('destinations') ) ];
}
$self->users->use_history(
uid => $user->{id},
set => $use_history
uid => $user->{id},
set => $use_history,
destinations => $destinations
);
$self->flash( success => 'use_history' );
$self->redirect_to('account');
}
$self->param( on_departure => $use_history & 0x01 ? 1 : 0 );
$self->param( on_arrival => $use_history & 0x02 ? 1 : 0 );
$self->param( on_departure => $use_history & 0x01 ? 1 : 0 );
$self->param( on_arrival => $use_history & 0x02 ? 1 : 0 );
$self->param( local_transit => $use_history & 0x04 ? 1 : 0 );
$self->param( destinations => join( "\n", @{$destinations} ) );
$self->render('use_history');
}

View file

@ -28,8 +28,11 @@ sub has_str_in_list {
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 $uid = $opt{uid} //= $self->current_user->{id};
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 $now = $self->now->epoch;
@ -72,7 +75,7 @@ sub get_connecting_trains_p {
@destinations = grep { $_ ne $exclude_via } @destinations;
}
if ( not @destinations ) {
if ( not( @destinations or $use_history & 0x04 and @{$lt_stops} ) ) {
return $promise->reject;
}
@ -82,43 +85,44 @@ sub get_connecting_trains_p {
my $iris_promise = Mojo::Promise->new;
$self->iris->get_departures_p(
station => $eva,
lookbehind => 10,
lookahead => $lookahead,
with_related => 1
)->then(
sub {
my ($stationboard) = @_;
if ( $stationboard->{errstr} ) {
$iris_promise->reject( $stationboard->{errstr} );
return;
}
if (@destinations) {
$self->iris->get_departures_p(
station => $eva,
lookbehind => 10,
lookahead => $lookahead,
with_related => 1
)->then(
sub {
my ($stationboard) = @_;
if ( $stationboard->{errstr} ) {
$iris_promise->reject( $stationboard->{errstr} );
return;
}
@{ $stationboard->{results} } = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [ $_, $_->departure ? $_->departure->epoch : 0 ] }
@{ $stationboard->{results} };
my @results;
my @cancellations;
my $excluded_train;
my %via_count = map { $_ => 0 } @destinations;
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;
}
@{ $stationboard->{results} } = map { $_->[0] }
sort { $a->[1] <=> $b->[1] }
map { [ $_, $_->departure ? $_->departure->epoch : 0 ] }
@{ $stationboard->{results} };
my @results;
my @cancellations;
my $excluded_train;
my %via_count = map { $_ => 0 } @destinations;
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
@ -136,97 +140,105 @@ sub get_connecting_trains_p {
# 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, @via ) ) {
push( @cancellations, [ $train, $dest ] );
next;
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, @via ) ) {
push( @cancellations, [ $train, $dest ] );
next;
}
}
}
}
else {
my @via = ( $train->route_post, $train->route_end );
for my $dest (@destinations) {
if ( $via_count{$dest} < 2
and has_str_in_list( $dest, @via ) )
{
push( @results, [ $train, $dest ] );
else {
my @via = ( $train->route_post, $train->route_end );
for my $dest (@destinations) {
if ( $via_count{$dest} < 2
and has_str_in_list( $dest, @via ) )
{
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}++;
if ( not $train->departure
or $train->departure->epoch >= $now )
{
$via_count{$dest}++;
}
next;
}
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;
@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} ) {
$interchange_duration
= $stationinfo->{i}{$arr_platform}{ $train->platform };
$interchange_duration //= $stationinfo->{i}{"*"};
# 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;
}
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';
# 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}{"*"};
}
elsif ( $interchange_time == $interchange_duration ) {
$train->{interchange_text}
= 'Anschluss könnte knapp werden';
$train->{interchange_icon} = 'directions_run';
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->reject(@_);
return;
}
)->wait;
$iris_promise->resolve( [ @results, @cancellations ] );
return;
}
)->catch(
sub {
$iris_promise->reject(@_);
return;
}
)->wait;
}
else {
$iris_promise->resolve( [] );
}
my $hafas_promise = Mojo::Promise->new;
my $rest_api = $self->config->{backend}{hafas_rest_api};
@ -254,6 +266,7 @@ sub get_connecting_trains_p {
my ( $iris, $hafas ) = @_;
my @iris_trains = @{ $iris->[0] };
my @hafas_trains = @{ $hafas->[0] };
my @transit_fyi;
my $strp = DateTime::Format::Strptime->new(
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 ($@) {
$self->app->log->error(
@ -303,7 +354,7 @@ sub get_connecting_trains_p {
);
}
$promise->resolve( \@iris_trains );
$promise->resolve( \@iris_trains, \@transit_fyi );
return;
}
)->catch(

View file

@ -9,6 +9,7 @@ use warnings;
use 5.020;
use DateTime;
use JSON;
my @sb_templates = (
undef,
@ -483,12 +484,34 @@ sub use_history {
my $uid = $opt{uid};
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) {
$db->update( 'users', { use_history => $value }, { id => $uid } );
}
else {
return $db->select( 'users', ['use_history'], { id => $uid } )
->hash->{use_history};
if ( $opt{with_local_transit} ) {
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;
% }
% 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) {
<p style="margin-top: 2ex;">
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>
<td>
<a href="/account/insight"><i class="material-icons">edit</i></a>
% if ($use_history & 0x03) {
% if ($use_history & 0x07) {
Vorschläge aktiv
% }
% else {

View file

@ -47,6 +47,30 @@
ohne Umweg über die Abfahrtstafel möglich.
</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="col s3 m3 l3">
</div>