From 3abe6aed5cf4ddb37fcfc1c03df59c57e5867c9c Mon Sep 17 00:00:00 2001 From: Daniel Friesel Date: Tue, 27 Dec 2022 11:07:16 +0100 Subject: it's a secret to everybody. --- lib/Travelynx.pm | 1 + lib/Travelynx/Controller/Traveling.pm | 84 ++++++++++++-- lib/Travelynx/Model/Journeys.pm | 213 +++++++++++++++++++++++++++++++++- 3 files changed, 289 insertions(+), 9 deletions(-) (limited to 'lib') diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index 012658b..db87ad6 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -2225,6 +2225,7 @@ sub startup { $authed_r->get('/history/commute')->to('traveling#commute'); $authed_r->get('/history/map')->to('traveling#map_history'); $authed_r->get('/history/:year')->to('traveling#yearly_history'); + $authed_r->get('/history/:year/review')->to('traveling#year_in_review'); $authed_r->get('/history/:year/:month')->to('traveling#monthly_history'); $authed_r->get('/journey/add')->to('traveling#add_journey_form'); $authed_r->get('/journey/comment')->to('traveling#comment_form'); diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index aa25e5c..7a00cd0 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -1448,11 +1448,73 @@ sub csv_history { ); } +sub year_in_review { + my ($self) = @_; + my $year = $self->stash('year'); + my @journeys; + + # DateTime is very slow when looking far into the future due to DST changes + # -> Limit time range to avoid accidental DoS. + if ( not( $year =~ m{ ^ [0-9]{4} $ }x and $year > 1990 and $year < 2100 ) ) + { + $self->render('not_found'); + return; + } + + my $interval_start = DateTime->new( + time_zone => 'Europe/Berlin', + year => $year, + month => 1, + day => 1, + hour => 0, + minute => 0, + second => 0, + ); + my $interval_end = $interval_start->clone->add( years => 1 ); + @journeys = $self->journeys->get( + uid => $self->current_user->{id}, + after => $interval_start, + before => $interval_end, + with_datetime => 1 + ); + + if ( not @journeys ) { + $self->render( 'not_found', + message => 'Keine Zugfahrten im angefragten Jahr gefunden.' ); + return; + } + + 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' + ); + return; + } + + my ( $stats, $review ) = $self->journeys->get_stats( + uid => $self->current_user->{id}, + year => $year, + review => 1 + ); + + $self->render( + 'year_in_review', + title => "travelynx Jahresrückblick $year", + year => $year, + stats => $stats, + review => $review + ); + +} + sub yearly_history { my ($self) = @_; my $year = $self->stash('year'); 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. @@ -1484,11 +1546,17 @@ sub yearly_history { return; } - $stats = $self->journeys->get_stats( + 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( json => { json => { @@ -1497,10 +1565,11 @@ sub yearly_history { } }, any => { - template => 'history_by_year', - journeys => [@journeys], - year => $year, - statistics => $stats + template => 'history_by_year', + journeys => [@journeys], + year => $year, + have_review => $with_review, + statistics => $stats } ); @@ -1511,7 +1580,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) @@ -1552,7 +1620,7 @@ sub monthly_history { return; } - $stats = $self->journeys->get_stats( + my $stats = $self->journeys->get_stats( uid => $self->current_user->{id}, year => $year, month => $month diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index b6647d2..1ab179a 100755 --- a/lib/Travelynx/Model/Journeys.pm +++ b/lib/Travelynx/Model/Journeys.pm @@ -34,6 +34,53 @@ sub epoch_to_dt { ); } +sub min_to_human { + my ($minutes) = @_; + + my @ret; + + if ( $minutes >= 14 * 24 * 60 ) { + push( @ret, int( $minutes / ( 7 * 24 * 60 ) ) . ' Wochen' ); + } + elsif ( $minutes >= 7 * 24 * 60 ) { + push( @ret, '1 Woche' ); + } + $minutes %= 7 * 24 * 60; + + if ( $minutes >= 2 * 24 * 60 ) { + push( @ret, int( $minutes / ( 24 * 60 ) ) . ' Tage' ); + } + elsif ( $minutes >= 24 * 60 ) { + push( @ret, '1 Tag' ); + } + $minutes %= 24 * 60; + + if ( $minutes >= 2 * 60 ) { + push( @ret, int( $minutes / 60 ) . ' Stunden' ); + } + elsif ( $minutes >= 60 ) { + push( @ret, '1 Stunde' ); + } + $minutes %= 60; + + if ( $minutes >= 2 ) { + push( @ret, "$minutes Minuten" ); + } + elsif ($minutes) { + push( @ret, "1 Minute" ); + } + + if ( @ret == 1 ) { + return $ret[0]; + } + + if ( @ret > 2 ) { + my $last = pop(@ret); + return join( ', ', @ret ) . " und $last"; + } + return "$ret[0] und $ret[1]"; +} + sub new { my ( $class, %opt ) = @_; @@ -991,6 +1038,165 @@ sub get_travel_distance { $distance_beeline, $skipped ); } +sub compute_review { + my ( $self, $stats, @journeys ) = @_; + my $longest_km; + my $longest_t; + my $shortest_km; + my $shortest_t; + my $message_count + ; # anzahl fahrten bei denen irgendeine nachricht vermerkt war -> irgendwas war anders als geplant + my %num_by_message; # für jede nachricht + my %num_by_wrtype + ; # zugtyp, sofern wagenreihung verfügbar. 'none' für nicht verfügbar. + my %num_by_linetype; # zugtyp nach "ICE 123" / "RE 127". + my %num_by_stop; # arr/dep name + + if ( not $stats or not @journeys or $stats->{num_trains} == 0 ) { + return; + } + + my %review; + + my $trains_per_journey = $stats->{num_trains} / $stats->{num_journeys}; + my $avg_change_count = sprintf( '%.1f', $trains_per_journey - 1 ); + my $min_total = $stats->{min_travel_real} + $stats->{min_interchange_real}; + + for my $journey (@journeys) { + if ( $journey->{rt_duration} ) { + if ( not $longest_t + or $journey->{rt_duration} > $longest_t->{rt_duration} ) + { + $longest_t = $journey; + } + if ( not $shortest_t + or $journey->{rt_duration} < $shortest_t->{rt_duration} ) + { + $shortest_t = $journey; + } + } + if ( $journey->{km_route} ) { + if ( not $longest_km + or $journey->{km_route} > $longest_km->{km_route} ) + { + $longest_km = $journey; + } + if ( not $shortest_km + or $journey->{km_route} < $shortest_km->{km_route} ) + { + $shortest_km = $journey; + } + } + if ( $journey->{messages} and @{ $journey->{messages} } ) { + $message_count += 1; + for my $message ( @{ $journey->{messages} } ) { + $num_by_message{ $message->[1] } += 1; + } + } + if ( $journey->{type} ) { + $num_by_linetype{ $journey->{type} } += 1; + } + if ( $journey->{from_name} ) { + $num_by_stop{ $journey->{from_name} } += 1; + } + if ( $journey->{to_name} ) { + $num_by_stop{ $journey->{to_name} } += 1; + } + } + + my @linetypes = sort { $b->[1] <=> $a->[1] } + map { [ $_, $num_by_linetype{$_} ] } keys %num_by_linetype; + my @stops = sort { $b->[1] <=> $a->[1] } + map { [ $_, $num_by_stop{$_} ] } keys %num_by_stop; + + my @reasons = sort { $b->[1] <=> $a->[1] } + map { [ $_, $num_by_message{$_} ] } keys %num_by_message; + + $review{num_stops} = scalar @stops; + $review{trains_per_day} = sprintf( '%.1f', $stats->{num_trains} / 365 ); + $review{km_route} = sprintf( '%.0f', $stats->{km_route} ); + $review{km_beeline} = sprintf( '%.0f', $stats->{km_beeline} ); + $review{km_circle} = sprintf( '%.1f', $stats->{km_route} / 40030 ); + $review{km_diag} = sprintf( '%.1f', $stats->{km_route} / 12742 ); + + $review{traveling_min_total} = $min_total; + $review{traveling_percentage_year} + = sprintf( "%.1f%%", $min_total * 100 / 525948.77 ); + $review{traveling_time_year} = min_to_human($min_total); + + if (@linetypes) { + $review{typical_type} = $linetypes[0][0]; + } + if ( @stops >= 3 ) { + my $desc = q{}; + $review{typical_stops_3} = [ $stops[0][0], $stops[1][0], $stops[2][0] ]; + } + elsif ( @stops == 2 ) { + $review{typical_stops_2} = [ $stops[0][0], $stops[1][0] ]; + } + $review{typical_time} + = min_to_human( $stats->{min_travel_real} / $stats->{num_trains} ); + $review{typical_km} + = sprintf( '%.0f', $stats->{km_route} / $stats->{num_trains} ); + $review{typical_kmh} = sprintf( '%.0f', + $stats->{km_route} / ( $stats->{min_travel_real} / 60 ) ); + $review{typical_delay_dep} + = sprintf( '%.0f', $stats->{delay_dep} / $stats->{num_trains} ); + $review{typical_delay_dep_h} = min_to_human( $review{typical_delay_dep} ); + $review{typical_delay_arr} + = sprintf( '%.0f', $stats->{delay_arr} / $stats->{num_trains} ); + $review{typical_delay_arr_h} = min_to_human( $review{typical_delay_arr} ); + + $review{longest_t_time} = min_to_human( $longest_t->{rt_duration} / 60 ); + $review{longest_t_type} = $longest_t->{type}; + $review{longest_t_lineno} = $longest_t->{line} // $longest_t->{no}; + $review{longest_t_from} = $longest_t->{from_name}; + $review{longest_t_to} = $longest_t->{to_name}; + $review{longest_t_id} = $longest_t->{id}; + + $review{longest_km_km} = sprintf( '%.0f', $longest_km->{km_route} ); + $review{longest_km_type} = $longest_km->{type}; + $review{longest_km_lineno} = $longest_km->{line} // $longest_km->{no}; + $review{longest_km_from} = $longest_km->{from_name}; + $review{longest_km_to} = $longest_km->{to_name}; + $review{longest_km_id} = $longest_km->{id}; + + $review{shortest_t_time} = min_to_human( $shortest_t->{rt_duration} / 60 ); + $review{shortest_t_type} = $shortest_t->{type}; + $review{shortest_t_lineno} = $shortest_t->{line} // $shortest_t->{no}; + $review{shortest_t_from} = $shortest_t->{from_name}; + $review{shortest_t_to} = $shortest_t->{to_name}; + $review{shortest_t_id} = $shortest_t->{id}; + + $review{shortest_km_m} = sprintf( '%.0f', $shortest_km->{km_route} * 1000 ); + $review{shortest_km_type} = $shortest_km->{type}; + $review{shortest_km_lineno} = $shortest_km->{line} // $shortest_km->{no}; + $review{shortest_km_from} = $shortest_km->{from_name}; + $review{shortest_km_to} = $shortest_km->{to_name}; + $review{shortest_km_id} = $shortest_km->{id}; + + $review{issue_percent} + = sprintf( '%.0f%%', $message_count * 100 / $stats->{num_trains} ); + for my $i ( 0 .. 2 ) { + if ( $reasons[$i] ) { + my $p = 'issue' . ( $i + 1 ); + $review{"${p}_count"} = $reasons[$i][1]; + $review{"${p}_text"} = $reasons[$i][0]; + } + } + + printf( "In %.0f%% der Fahrten war irgendetwas nicht wie vorgesehen\n", + $message_count * 100 / $stats->{num_trains} ); + say "Die drei häufigsten Anmerkungen waren:"; + for my $i ( 0 .. 2 ) { + if ( $reasons[$i] ) { + printf( "%d× %s\n", $reasons[$i][1], $reasons[$i][0] ); + } + } + + return \%review; +} + sub compute_stats { my ( $self, @journeys ) = @_; my $km_route = 0; @@ -1093,7 +1299,8 @@ sub get_stats { # checks out of a train or manually edits/adds a journey. if ( - not $opt{write_only} + not $opt{write_only} + and not $opt{review} and my $stats = $self->stats_cache->get( uid => $uid, db => $db, @@ -1148,6 +1355,10 @@ sub get_stats { stats => $stats ); + if ( $opt{review} ) { + return ( $stats, $self->compute_review( $stats, @journeys ) ); + } + return $stats; } -- cgit v1.2.3