diff options
Diffstat (limited to 'lib/Travelynx.pm')
-rwxr-xr-x | lib/Travelynx.pm | 703 |
1 files changed, 25 insertions, 678 deletions
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index d0b268e..232cb46 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -10,16 +10,16 @@ use DateTime; use DateTime::Format::Strptime; use Encode qw(decode encode); use File::Slurp qw(read_file); -use Geo::Distance; use JSON; use List::Util; use List::UtilsBy qw(uniq_by); -use List::MoreUtils qw(after_incl before_incl first_index); +use List::MoreUtils qw(first_index); use Travel::Status::DE::DBWagenreihung; use Travel::Status::DE::IRIS::Stations; use Travelynx::Helper::HAFAS; use Travelynx::Helper::IRIS; use Travelynx::Helper::Sendmail; +use Travelynx::Model::Journeys; use Travelynx::Model::Users; use XML::LibXML; @@ -290,6 +290,18 @@ sub startup { ); $self->helper( + journeys => sub { + my ($self) = @_; + state $journeys = Travelynx::Model::Journeys->new( + log => $self->app->log, + pg => $self->pg, + renamed_station => $self->app->renamed_station, + station_by_eva => $self->app->station_by_eva, + ); + } + ); + + $self->helper( pg => sub { my ($self) = @_; my $config = $self->app->config; @@ -359,126 +371,6 @@ sub startup { } ); - # Returns (journey id, error) - # Must be called during a transaction. - # Must perform a rollback on error. - $self->helper( - 'add_journey' => sub { - my ( $self, %opt ) = @_; - - my $db = $opt{db}; - my $uid = $opt{uid} // $self->current_user->{id}; - my $now = DateTime->now( time_zone => 'Europe/Berlin' ); - my $dep_station = get_station( $opt{dep_station} ); - my $arr_station = get_station( $opt{arr_station} ); - - if ( not $dep_station ) { - return ( undef, 'Unbekannter Startbahnhof' ); - } - if ( not $arr_station ) { - return ( undef, 'Unbekannter Zielbahnhof' ); - } - - my $daily_journey_count = $db->select( - 'journeys_str', - 'count(*) as count', - { - user_id => $uid, - real_dep_ts => { - -between => [ - $opt{rt_departure}->clone->subtract( days => 1 ) - ->epoch, - $opt{rt_departure}->epoch - ], - }, - } - )->hash->{count}; - - if ( $daily_journey_count >= 100 ) { - return ( undef, -"In den 24 Stunden vor der angegebenen Abfahrtszeit wurden ${daily_journey_count} weitere Fahrten angetreten. Das kann nicht stimmen." - ); - } - - my @route = ( [ $dep_station->[1], {}, undef ] ); - - if ( $opt{route} ) { - my @unknown_stations; - for my $station ( @{ $opt{route} } ) { - my $station_info = get_station($station); - if ($station_info) { - push( @route, [ $station_info->[1], {}, undef ] ); - } - else { - push( @route, [ $station, {}, undef ] ); - push( @unknown_stations, $station ); - } - } - - if ( not $opt{lax} ) { - if ( @unknown_stations == 1 ) { - return ( undef, - "Unbekannter Unterwegshalt: $unknown_stations[0]" ); - } - elsif (@unknown_stations) { - return ( undef, - 'Unbekannte Unterwegshalte: ' - . join( ', ', @unknown_stations ) ); - } - } - } - - push( @route, [ $arr_station->[1], {}, undef ] ); - - if ( $route[0][0] eq $route[1][0] ) { - shift(@route); - } - - if ( $route[-2][0] eq $route[-1][0] ) { - pop(@route); - } - - my $entry = { - user_id => $uid, - train_type => $opt{train_type}, - train_line => $opt{train_line}, - train_no => $opt{train_no}, - train_id => 'manual', - checkin_station_id => $dep_station->[2], - checkin_time => $now, - sched_departure => $opt{sched_departure}, - real_departure => $opt{rt_departure}, - checkout_station_id => $arr_station->[2], - sched_arrival => $opt{sched_arrival}, - real_arrival => $opt{rt_arrival}, - checkout_time => $now, - edited => 0x3fff, - cancelled => $opt{cancelled} ? 1 : 0, - route => JSON->new->encode( \@route ), - }; - - if ( $opt{comment} ) { - $entry->{user_data} - = JSON->new->encode( { comment => $opt{comment} } ); - } - - my $journey_id = undef; - eval { - $journey_id - = $db->insert( 'journeys', $entry, { returning => 'id' } ) - ->hash->{id}; - $self->invalidate_stats_cache( $opt{rt_departure}, $db, $uid ); - }; - - if ($@) { - $self->app->log->error("add_journey($uid): $@"); - return ( undef, 'add_journey failed: ' . $@ ); - } - - return ( $journey_id, undef ); - } - ); - $self->helper( 'checkin' => sub { my ( $self, $station, $train_id, $uid ) = @_; @@ -620,7 +512,11 @@ sub startup { ); } - $self->invalidate_stats_cache( $cache_ts, $db, $uid ); + $self->journeys->invalidate_stats_cache( + ts => $cache_ts, + db => $db, + uid => $uid + ); $tx->commit; }; @@ -633,36 +529,6 @@ sub startup { } ); - # Statistics are partitioned by real_departure, which must be provided - # when calling this function e.g. after journey deletion or editing. - # If a joureny's real_departure has been edited, this function must be - # called twice: once with the old and once with the new value. - $self->helper( - 'invalidate_stats_cache' => sub { - my ( $self, $ts, $db, $uid ) = @_; - - $uid //= $self->current_user->{id}; - $db //= $self->pg->db; - - $self->pg->db->delete( - 'journey_stats', - { - user_id => $uid, - year => $ts->year, - month => $ts->month, - } - ); - $self->pg->db->delete( - 'journey_stats', - { - user_id => $uid, - year => $ts->year, - month => 0, - } - ); - } - ); - $self->helper( 'checkout' => sub { my ( $self, $station, $force, $uid ) = @_; @@ -856,7 +722,11 @@ sub startup { month => $+{month} ); } - $self->invalidate_stats_cache( $cache_ts, $db, $uid ); + $self->journeys->invalidate_stats_cache( + ts => $cache_ts, + db => $db, + uid => $uid + ); } elsif ( defined $train and $train->arrival_is_cancelled ) { @@ -929,208 +799,6 @@ sub startup { } ); - $self->helper( - 'update_journey_part' => sub { - my ( $self, $db, $journey_id, $key, $value ) = @_; - my $rows; - - my $journey = $self->get_journey( - db => $db, - journey_id => $journey_id, - with_datetime => 1, - ); - - eval { - if ( $key eq 'from_name' ) { - my $from_station = get_station( $value, 1 ); - if ( not $from_station ) { - die("Unbekannter Startbahnhof\n"); - } - $rows = $db->update( - 'journeys', - { - checkin_station_id => $from_station->[2], - edited => $journey->{edited} | 0x0004, - }, - { - id => $journey_id, - } - )->rows; - } - elsif ( $key eq 'to_name' ) { - my $to_station = get_station( $value, 1 ); - if ( not $to_station ) { - die("Unbekannter Zielbahnhof\n"); - } - $rows = $db->update( - 'journeys', - { - checkout_station_id => $to_station->[2], - edited => $journey->{edited} | 0x0400, - }, - { - id => $journey_id, - } - )->rows; - } - elsif ( $key eq 'sched_departure' ) { - $rows = $db->update( - 'journeys', - { - sched_departure => $value, - edited => $journey->{edited} | 0x0001, - }, - { - id => $journey_id, - } - )->rows; - } - elsif ( $key eq 'rt_departure' ) { - $rows = $db->update( - 'journeys', - { - real_departure => $value, - edited => $journey->{edited} | 0x0002, - }, - { - id => $journey_id, - } - )->rows; - - # stats are partitioned by rt_departure -> both the cache for - # the old value (see bottom of this function) and the new value - # (here) must be invalidated. - $self->invalidate_stats_cache( $value, $db ); - } - elsif ( $key eq 'sched_arrival' ) { - $rows = $db->update( - 'journeys', - { - sched_arrival => $value, - edited => $journey->{edited} | 0x0100, - }, - { - id => $journey_id, - } - )->rows; - } - elsif ( $key eq 'rt_arrival' ) { - $rows = $db->update( - 'journeys', - { - real_arrival => $value, - edited => $journey->{edited} | 0x0200, - }, - { - id => $journey_id, - } - )->rows; - } - elsif ( $key eq 'route' ) { - my @new_route = map { [ $_, {}, undef ] } @{$value}; - $rows = $db->update( - 'journeys', - { - route => JSON->new->encode( \@new_route ), - edited => $journey->{edited} | 0x0010, - }, - { - id => $journey_id, - } - )->rows; - } - elsif ( $key eq 'cancelled' ) { - $rows = $db->update( - 'journeys', - { - cancelled => $value, - edited => $journey->{edited} | 0x0020, - }, - { - id => $journey_id, - } - )->rows; - } - elsif ( $key eq 'comment' ) { - $journey->{user_data}{comment} = $value; - $rows = $db->update( - 'journeys', - { - user_data => - JSON->new->encode( $journey->{user_data} ), - }, - { - id => $journey_id, - } - )->rows; - } - else { - die("Invalid key $key\n"); - } - }; - - if ($@) { - $self->app->log->error( - "update_journey_part($journey_id, $key): $@"); - return "update_journey_part($key): $@"; - } - if ( $rows == 1 ) { - $self->invalidate_stats_cache( $journey->{rt_departure}, $db ); - return undef; - } - return 'UPDATE failed: did not match any journey part'; - } - ); - - $self->helper( - 'journey_sanity_check' => sub { - my ( $self, $journey, $lax ) = @_; - - if ( defined $journey->{sched_duration} - and $journey->{sched_duration} <= 0 ) - { - return -'Die geplante Dauer dieser Zugfahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.'; - } - if ( defined $journey->{rt_duration} - and $journey->{rt_duration} <= 0 ) - { - return -'Die Dauer dieser Zugfahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.'; - } - if ( $journey->{sched_duration} - and $journey->{sched_duration} > 60 * 60 * 24 ) - { - return 'Die Zugfahrt ist länger als 24 Stunden.'; - } - if ( $journey->{rt_duration} - and $journey->{rt_duration} > 60 * 60 * 24 ) - { - return 'Die Zugfahrt ist länger als 24 Stunden.'; - } - if ( $journey->{kmh_route} > 500 or $journey->{kmh_beeline} > 500 ) - { - return 'Zugfahrten mit über 500 km/h? Schön wär\'s.'; - } - if ( $journey->{route} and @{ $journey->{route} } > 99 ) { - my $stop_count = @{ $journey->{route} }; - return -"Die Zugfahrt hat $stop_count Unterwegshalte. Also ich weiß ja nicht so recht."; - } - if ( $journey->{edited} & 0x0010 and not $lax ) { - my @unknown_stations - = $self->grep_unknown_stations( map { $_->[0] } - @{ $journey->{route} } ); - if (@unknown_stations) { - return 'Unbekannte Station(en): ' - . join( ', ', @unknown_stations ); - } - } - - return undef; - } - ); - # This helper should only be called directly when also providing a user ID. # If you don't have one, use current_user() instead (get_user_data will # delegate to it anyways). @@ -1292,55 +960,6 @@ sub startup { ); $self->helper( - 'delete_journey' => sub { - my ( $self, $journey_id, $checkin_epoch, $checkout_epoch ) = @_; - my $uid = $self->current_user->{id}; - - my @journeys = $self->get_user_travels( - uid => $uid, - journey_id => $journey_id - ); - if ( @journeys == 0 ) { - return 'Journey not found'; - } - my $journey = $journeys[0]; - - # Double-check (comparing both ID and action epoch) to make sure we - # are really deleting the right journey and the user isn't just - # playing around with POST requests. - if ( $journey->{id} != $journey_id - or $journey->{checkin_ts} != $checkin_epoch - or $journey->{checkout_ts} != $checkout_epoch ) - { - return 'Invalid journey data'; - } - - my $rows; - eval { - $rows = $self->pg->db->delete( - 'journeys', - { - user_id => $uid, - id => $journey_id, - } - )->rows; - }; - - if ($@) { - $self->app->log->error("Delete($uid, $journey_id): $@"); - return 'DELETE failed: ' . $@; - } - - if ( $rows == 1 ) { - $self->invalidate_stats_cache( - epoch_to_dt( $journey->{rt_dep_ts} ) ); - return undef; - } - return sprintf( 'Deleted %d rows, expected 1', $rows ); - } - ); - - $self->helper( 'get_journey_stats' => sub { my ( $self, %opt ) = @_; @@ -1401,7 +1020,7 @@ sub startup { $interval_end = $interval_start->clone->add( years => 1 ); } - my @journeys = $self->get_user_travels( + my @journeys = $self->journeys->get( uid => $uid, cancelled => $opt{cancelled} ? 1 : 0, verbose => 1, @@ -2002,31 +1621,6 @@ sub startup { ); $self->helper( - 'get_oldest_journey_ts' => sub { - my ($self) = @_; - - my $res_h = $self->pg->db->select( - 'journeys_str', - ['sched_dep_ts'], - { - user_id => $self->current_user->{id}, - }, - { - limit => 1, - order_by => { - -asc => 'real_dep_ts', - }, - } - )->hash; - - if ($res_h) { - return epoch_to_dt( $res_h->{sched_dep_ts} ); - } - return undef; - } - ); - - $self->helper( 'get_latest_dest_id' => sub { my ( $self, %opt ) = @_; @@ -2254,164 +1848,6 @@ sub startup { ); $self->helper( - 'get_user_travels' => sub { - my ( $self, %opt ) = @_; - - my $uid = $opt{uid} || $self->current_user->{id}; - - # If get_user_travels is called from inside a transaction, db - # specifies the database handle performing the transaction. - # Otherwise, we grab a fresh one. - my $db = $opt{db} // $self->pg->db; - - my @select - = ( - qw(journey_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva checkout_ts sched_arr_ts real_arr_ts arr_eva cancelled edited route messages user_data) - ); - my %where = ( - user_id => $uid, - cancelled => 0 - ); - my %order = ( - order_by => { - -desc => 'real_dep_ts', - } - ); - - if ( $opt{cancelled} ) { - $where{cancelled} = 1; - } - - if ( $opt{limit} ) { - $order{limit} = $opt{limit}; - } - - if ( $opt{journey_id} ) { - $where{journey_id} = $opt{journey_id}; - delete $where{cancelled}; - } - elsif ( $opt{after} and $opt{before} ) { - $where{real_dep_ts} = { - -between => [ $opt{after}->epoch, $opt{before}->epoch, ] }; - } - - if ( $opt{with_polyline} ) { - push( @select, 'polyline' ); - } - - my @travels; - - my $res = $db->select( 'journeys_str', \@select, \%where, \%order ); - - for my $entry ( $res->expand->hashes->each ) { - - my $ref = { - id => $entry->{journey_id}, - type => $entry->{train_type}, - line => $entry->{train_line}, - no => $entry->{train_no}, - from_eva => $entry->{dep_eva}, - checkin_ts => $entry->{checkin_ts}, - sched_dep_ts => $entry->{sched_dep_ts}, - rt_dep_ts => $entry->{real_dep_ts}, - to_eva => $entry->{arr_eva}, - checkout_ts => $entry->{checkout_ts}, - sched_arr_ts => $entry->{sched_arr_ts}, - rt_arr_ts => $entry->{real_arr_ts}, - messages => $entry->{messages}, - route => $entry->{route}, - edited => $entry->{edited}, - user_data => $entry->{user_data}, - }; - - if ( $opt{with_polyline} ) { - $ref->{polyline} = $entry->{polyline}; - } - - if ( my $station - = $self->app->station_by_eva->{ $ref->{from_eva} } ) - { - $ref->{from_ds100} = $station->[0]; - $ref->{from_name} = $station->[1]; - } - if ( my $station - = $self->app->station_by_eva->{ $ref->{to_eva} } ) - { - $ref->{to_ds100} = $station->[0]; - $ref->{to_name} = $station->[1]; - } - - if ( $opt{with_datetime} ) { - $ref->{checkin} = epoch_to_dt( $ref->{checkin_ts} ); - $ref->{sched_departure} - = epoch_to_dt( $ref->{sched_dep_ts} ); - $ref->{rt_departure} = epoch_to_dt( $ref->{rt_dep_ts} ); - $ref->{checkout} = epoch_to_dt( $ref->{checkout_ts} ); - $ref->{sched_arrival} = epoch_to_dt( $ref->{sched_arr_ts} ); - $ref->{rt_arrival} = epoch_to_dt( $ref->{rt_arr_ts} ); - } - - if ( $opt{verbose} ) { - my $rename = $self->app->renamed_station; - for my $stop ( @{ $ref->{route} } ) { - if ( $rename->{ $stop->[0] } ) { - $stop->[0] = $rename->{ $stop->[0] }; - } - } - $ref->{cancelled} = $entry->{cancelled}; - my @parsed_messages; - for my $message ( @{ $ref->{messages} // [] } ) { - my ( $ts, $msg ) = @{$message}; - push( @parsed_messages, [ epoch_to_dt($ts), $msg ] ); - } - $ref->{messages} = [ reverse @parsed_messages ]; - $ref->{sched_duration} - = defined $ref->{sched_arr_ts} - ? $ref->{sched_arr_ts} - $ref->{sched_dep_ts} - : undef; - $ref->{rt_duration} - = defined $ref->{rt_arr_ts} - ? $ref->{rt_arr_ts} - $ref->{rt_dep_ts} - : undef; - my ( $km_polyline, $km_route, $km_beeline, $skip ) - = $self->get_travel_distance($ref); - $ref->{km_route} = $km_polyline || $km_route; - $ref->{skip_route} = $km_polyline ? 0 : $skip; - $ref->{km_beeline} = $km_beeline; - $ref->{skip_beeline} = $skip; - my $kmh_divisor - = ( $ref->{rt_duration} // $ref->{sched_duration} - // 999999 ) / 3600; - $ref->{kmh_route} - = $kmh_divisor ? $ref->{km_route} / $kmh_divisor : -1; - $ref->{kmh_beeline} - = $kmh_divisor - ? $ref->{km_beeline} / $kmh_divisor - : -1; - } - - push( @travels, $ref ); - } - - return @travels; - } - ); - - $self->helper( - 'get_journey' => sub { - my ( $self, %opt ) = @_; - - $opt{cancelled} = 'any'; - my @journeys = $self->get_user_travels(%opt); - if ( @journeys == 0 ) { - return undef; - } - - return $journeys[0]; - } - ); - - $self->helper( 'stationinfo_to_direction' => sub { my ( $self, $platform_info, $wagonorder, $prev_stop, $next_stop ) = @_; @@ -3146,95 +2582,6 @@ sub startup { ); $self->helper( - 'get_travel_distance' => sub { - my ( $self, $journey ) = @_; - - my $from = $journey->{from_name}; - my $from_eva = $journey->{from_eva}; - my $to = $journey->{to_name}; - my $to_eva = $journey->{to_eva}; - my $route_ref = $journey->{route}; - my $polyline_ref = $journey->{polyline}; - - my $distance_polyline = 0; - my $distance_intermediate = 0; - my $distance_beeline = 0; - my $skipped = 0; - my $geo = Geo::Distance->new(); - my @stations = map { $_->[0] } @{$route_ref}; - my @route = after_incl { $_ eq $from } @stations; - @route = before_incl { $_ eq $to } @route; - - if ( @route < 2 ) { - - # I AM ERROR - return ( 0, 0, 0 ); - } - - my @polyline = after_incl { $_->[2] and $_->[2] == $from_eva } - @{ $polyline_ref // [] }; - @polyline - = before_incl { $_->[2] and $_->[2] == $to_eva } @polyline; - - my $prev_station = shift @polyline; - for my $station (@polyline) { - - #lonlatlonlat - $distance_polyline - += $geo->distance( 'kilometer', $prev_station->[0], - $prev_station->[1], $station->[0], $station->[1] ); - $prev_station = $station; - } - - $prev_station = get_station( shift @route ); - if ( not $prev_station ) { - return ( $distance_polyline, 0, 0 ); - } - - # Geo-coordinates for stations outside Germany are not available - # at the moment. When calculating distance with intermediate stops, - # these are simply left out (as if they were not part of the route). - # For beeline distance calculation, we use the route's first and last - # station with known geo-coordinates. - my $from_station_beeline; - my $to_station_beeline; - - # $#{$station} >= 4 iff $station has geocoordinates - for my $station_name (@route) { - if ( my $station = get_station($station_name) ) { - if ( not $from_station_beeline and $#{$prev_station} >= 4 ) - { - $from_station_beeline = $prev_station; - } - if ( $#{$station} >= 4 ) { - $to_station_beeline = $station; - } - if ( $#{$prev_station} >= 4 and $#{$station} >= 4 ) { - $distance_intermediate - += $geo->distance( 'kilometer', $prev_station->[3], - $prev_station->[4], $station->[3], $station->[4] ); - } - else { - $skipped++; - } - $prev_station = $station; - } - } - - if ( $from_station_beeline and $to_station_beeline ) { - $distance_beeline = $geo->distance( - 'kilometer', $from_station_beeline->[3], - $from_station_beeline->[4], $to_station_beeline->[3], - $to_station_beeline->[4] - ); - } - - return ( $distance_polyline, $distance_intermediate, - $distance_beeline, $skipped ); - } - ); - - $self->helper( 'compute_journey_stats' => sub { my ( $self, @journeys ) = @_; my $km_route = 0; |