summaryrefslogtreecommitdiff
path: root/lib/Travelynx.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx.pm')
-rwxr-xr-xlib/Travelynx.pm703
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;