Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FGR: account for cumulative delays ≥20 minutes #116

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/Travelynx.pm
Original file line number Diff line number Diff line change
Expand Up @@ -2459,6 +2459,7 @@ sub startup {
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
$authed_r->get('/cancelled')->to('traveling#cancelled');
$authed_r->get('/fgr')->to('passengerrights#list_candidates');
$authed_r->get('/fgr_zeitkarten')->to('passengerrights#list_cumulative_delays');
$authed_r->get('/account/password')->to('account#password_form');
$authed_r->get('/account/mail')->to('account#change_mail');
$authed_r->get('/account/name')->to('account#change_name');
Expand Down
128 changes: 128 additions & 0 deletions lib/Travelynx/Controller/Passengerrights.pm
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package Travelynx::Controller::Passengerrights;
use Mojo::Base 'Mojolicious::Controller';

use DateTime;
use List::Util;
use POSIX;
use CAM::PDF;

# Internal Helpers
Expand Down Expand Up @@ -160,6 +162,132 @@ sub list_candidates {
);
}

sub list_cumulative_delays {
my ($self) = @_;
my $parser = DateTime::Format::Strptime->new(
pattern => '%Y-%m-%d',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
);
my @fv_types = qw(IC ICE EC ECE RJ RJX D IR NJ TGV WB FLX);
my @not_train_types = qw(Bus STR STB U);
my $ticket_value = $self->param('ticket_value') // 150;

my $start = $self->param('start') ?
$parser->parse_datetime($self->param('start')) :
$self->now->truncate(to=>'month');

my $end = $self->param('end') ?
$parser->parse_datetime($self->param('end')) :
$self->now->truncate(to=>'month')->add(months=>1)->subtract(days=>1);

my @journeys = $self->journeys->get(
uid => $self->current_user->{id},
after => $start->clone,
before => $end->clone->add(days=>1),
with_datetime => 1,
);
# filter for realtime data
@journeys = grep { $_->{sched_arrival}->epoch and $_->{rt_arrival}->epoch }
@journeys;

# look for substitute connections after cancellation
# start by finding all canceled trains during ticket validity
my @cancelled = $self->journeys->get(
uid => $self->current_user->{id},
after => $start->clone,
before => $end->clone->add(days=>1),
cancelled => 1,
with_datetime => 1,
);
for my $journey (@cancelled) {
# filter out non-train transports not covered by FGR
if (List::Util::any {$journey->{type} eq $_} @not_train_types) {
next;
}
if ( not $journey->{sched_arrival}->epoch ) {
next;
}

# try to find a substitute connection for the canceled train
$journey->{cancelled} = 1;
$self->mark_substitute_connection($journey);
# if we have a substitute connection with real-time data, add the
# train to the eligible list
if ($journey->{has_substitute} and
$journey->{to_substitute}->{rt_arr_ts}) {
push( @journeys, $journey );
}
}

# sum up delays
my $cumulative_delay = 0;
for my $i ( 0 .. $#journeys ) {
my $journey = $journeys[$i];
# filter out non-train transports not covered by FGR
if (List::Util::any {$journey->{type} eq $_} @not_train_types) {
next;
}
# if we're using a regional ticket, filter out all long-distance trains
if ($ticket_value < 500 and List::Util::any {$journey->{type} eq $_} @fv_types) {
next;
}

$journey->{delay} = $journey->{substitute_delay} //
( $journey->{rt_arrival}->epoch - $journey->{sched_arrival}->epoch ) / 60;

# find candidates for missed connections - if we arrive with a delay and
# later check into a train again from the same station
# note that we can't assign a delay to potential missed connections
# because we don't know the planned arrival of the train we missed
if ( $i > 0 and $journey->{delay} >= 3) {
$self->mark_if_missed_connection( $journey, $journeys[ $i - 1 ] );
}

if ($journey->{delay} >= 20) {
# add up to 60 minutes of delay per journey
# not entirely clear if you could in theory get compensation
# for a single 180-minute-delayed journey, so let's play it safe
$cumulative_delay += ($journey->{delay} < 60) ? $journey->{delay} : 60;
$journey->{generate_fgr_target} = sprintf(
'/journey/passenger_rights/FGR %s %s %s.pdf',
$journey->{sched_departure}->ymd, $journey->{type}, $journey->{no}
);
} elsif ($journey->{connection_missed}) {
$journey->{generate_fgr_target} = sprintf(
'/journey/passenger_rights/FGR %s %s %s.pdf',
$journey->{sched_departure}->ymd, $journey->{type}, $journey->{no}
);
}
}
# filter out journeys with delay below +20
@journeys = grep { ($_->{delay} // 0) >= 20 or $_->{connection_missed} } @journeys;
# sort by departure - we did add all the substitute trains to the very back
@journeys = sort {$b->{rt_departure} cmp $a->{rt_departure}} @journeys;


my $compensation_amount = floor($cumulative_delay / 60) * $ticket_value;

my $min_delay_for_compensation = ceil(400/$ticket_value) * 60;
my $bar_fill = int( ($cumulative_delay/$min_delay_for_compensation) * 100);
$bar_fill = $bar_fill > 100 ? 100 : $bar_fill;

$self->render(
'passengerrights_cumulative',
title=>'travelynx: Fahrgastrechte Zeitkarten',
start=>$start,
end=>$end,
journeys=>[@journeys],
num_journeys=>scalar @journeys,
cumulative_delay=>$cumulative_delay,
compensation_amount=>$compensation_amount,
min_delay_for_compensation=>$min_delay_for_compensation,
bar_fill=>$bar_fill,
ticket_value=>$ticket_value,
did_miss_connections=>List::Util::any { $_->{connection_missed} } @journeys
);
}

sub generate {
my ($self) = @_;
my $journey_id = $self->param('id');
Expand Down
2 changes: 2 additions & 0 deletions lib/Travelynx/Controller/Traveling.pm
Original file line number Diff line number Diff line change
Expand Up @@ -1172,11 +1172,13 @@ sub cancelled {
cancelled => 1,
with_datetime => 1
);
foreach (@journeys) { $_->{cancelled} = 1; }

$self->respond_to(
json => { json => [@journeys] },
any => {
template => 'cancelled',
title => 'travelynx: Zugausfälle',
journeys => [@journeys]
}
);
Expand Down
1 change: 1 addition & 0 deletions public/static/js/travelynx-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,4 +317,5 @@ $(document).ready(function() {
fullWidth: true,
indicators: true}
);
$('select').formSelect();
});
47 changes: 35 additions & 12 deletions sass/src/common/local.scss
Original file line number Diff line number Diff line change
Expand Up @@ -133,41 +133,64 @@ ul.suggestions {
display: grid;
grid-template-columns: 10ch 1fr;
grid-template-rows: 1fr;
a:first-child {
a.history-line-link {
align-self: center;
text-align: center;
display: flex;
grid-column: 1;
}
ul.route-history {
grid-column: 2;
}
&.history-date-change {
display: block;
font-weight: bold;
}
}

.collection.history.fgr > li {
grid-template-columns: 10ch 1fr min-content;
.fgr-reason {
grid-column: 1 / span 2;
}
form {
grid-row: 1 / span 3;
grid-column: 3;
align-self: center;
}
}

ul.route-history > li {
list-style: none;

position: relative;
display: grid;
grid-template-columns: 1rem 1fr;
gap: 0.5rem;
a {
font-family: $font-stack;
}
strong {
font-weight: 600;
}
&.cancelled a strong {
font-style: italic;
font-weight: normal;
}

// route icon bubble
i.material-icons {
&[aria-label=nach] {
padding-top: 0.4rem;
}
&[aria-label=von] {
width: 1rem;
height: 1rem;
padding-top: 0.4rem;
// mini arrow
&[aria-label*=nach]::after {
content: '';
display: block;
transform: rotate(-90deg);
height: 1rem;
margin-top: 0.4rem;
position: absolute;
height: 6px;
width: 6px;
margin-left: 4px;
border: 2px solid $off-black;
border-width: 2px 2px 0 0;
rotate: -45deg;
}
}

Expand All @@ -182,7 +205,7 @@ ul.route-history > li {
top: 0;
}
&:first-of-type::before {
top: 1.3rem;
top: 1.4rem;
}
&:last-of-type::before {
bottom: unset;
Expand Down
84 changes: 45 additions & 39 deletions templates/_history_trains.html.ep
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
<div class="row">
<div class="col s12">
<ul class="collection history">
% my $fgr_class = '';
% if (@{$journeys} && @{$journeys}[0]->{generate_fgr_target}) {
% $fgr_class = 'fgr';
%}
<ul class="collection history <%= $fgr_class %>">
% my $olddate = '';
% for my $travel (@{$journeys}) {
% my $detail_link = '/journey/' . $travel->{id};
Expand All @@ -15,51 +19,53 @@
% $olddate = $date
% }
<li class="collection-item">
<a href="<%= $detail_link %>">

% # passenger rights history listing only:
% # display substitute connection or next train after possible missed
% # connection if applicable
% if (my $substitute = $travel->{to_substitute} // $travel->{connection}) {
% my $subst_detail_link = '/journey/' . $substitute->{id};
% if (my $prefix = stash('link_prefix')) {
% $subst_detail_link = $prefix . $substitute->{id};
% }
<a href="<%= $subst_detail_link %>" class="history-line-link unmarked">
<span class="dep-line <%= $substitute->{type} // q{} %>">
<%= $substitute->{type} %> <%= $substitute->{line} // $substitute->{no}%>
</span>
</a>
<ul class="route-history">
%= include '_route_history_item', stop_type => 'destination', travel => $substitute, link => $subst_detail_link
%= include '_route_history_item', stop_type => 'origin', travel => $substitute, link => $subst_detail_link
</ul>
<i class="fgr-reason">
% if ($travel->{has_substitute}) {
— Ersatzverbindung mit +<%= $travel->{substitute_delay}%> für —
% } elsif ($travel->{connection_missed}) {
— Erster Zug mit +<%= $travel->{delay}%>, eventuell in <%= $travel->{to_name} %> Anschluss verpasst —
% }
</i>
% }
% # end of passenger rights substitute connection

<a href="<%= $detail_link %>" class="history-line-link">
<span class="dep-line <%= $travel->{type} // q{} %>">
<%= $travel->{type} %> <%= $travel->{line} // $travel->{no}%>
</span>
</a>

<ul class="route-history">
<li>
<i class="material-icons tiny" aria-label="nach">radio_button_unchecked</i>

<a href="<%= $detail_link %>" class="unmarked">
% if (param('cancelled') and $travel->{sched_arrival}->epoch != 0) {
%= $travel->{sched_arrival}->strftime('%H:%M')
% }
% else {
% if ($travel->{rt_arrival}->epoch == 0 and $travel->{sched_arrival}->epoch == 0) {
<i class="material-icons">timer_off</i>
% } else {
%= $travel->{rt_arrival}->strftime('%H:%M');
% if ($travel->{sched_arrival} != $travel->{rt_arrival}) {
(<%= sprintf('%+d', ($travel->{rt_arrival}->epoch - $travel->{sched_arrival}->epoch) / 60) %>)
% }
% }
% }
<strong><%= $travel->{to_name} %></strong>
</a>
</li>

<li>
<i class="material-icons tiny" aria-label="von">play_circle_filled</i>

<a href="<%= $detail_link %>" class="unmarked">
% if (param('cancelled')) {
%= $travel->{sched_departure}->strftime('%H:%M')
% }
% else {
<%= $travel->{rt_departure}->strftime('%H:%M') %>
% if ($travel->{sched_departure} != $travel->{rt_departure}) {
(<%= sprintf('%+d', ($travel->{rt_departure}->epoch - $travel->{sched_departure}->epoch) / 60) %>)
% }
% }
<strong><%= $travel->{from_name} %></strong>
</a>
</li>
%= include '_route_history_item', stop_type => 'destination', travel => $travel, link => $detail_link, cancelled => $travel->{cancelled}
%= include '_route_history_item', stop_type => 'origin', travel => $travel, link => $detail_link, cancelled => $travel->{cancelled}
</ul>
% if ($travel->{generate_fgr_target}) {
%= form_for $travel->{generate_fgr_target} => (method => 'POST') => (target => '_blank') => begin
%= csrf_field
%= hidden_field id => $travel->{id}
<button class="btn waves-effect waves-light grey darken-3" type="submit" name="action" value="generate">
<i class="material-icons" aria-label="Formular herunterladen">file_download</i>
</button>
%= end
% }
</li>
% }
</ul>
Expand Down
Loading
Loading