diff options
| author | Daniel Friesel <derf@finalrewind.org> | 2020-08-16 21:20:23 +0200 | 
|---|---|---|
| committer | Daniel Friesel <derf@finalrewind.org> | 2020-08-16 21:20:23 +0200 | 
| commit | 47b957361e84c826bfee0fa4e45db9de9e5c0182 (patch) | |
| tree | 1ae4234091fbb321df7dafea395d962963ca2d96 | |
| parent | d9624ee30882a66e5304a2cca0591a77fc9445f1 (diff) | |
Move most journey code to newly introduced Journey model
| -rwxr-xr-x | lib/Travelynx.pm | 703 | ||||
| -rwxr-xr-x | lib/Travelynx/Controller/Api.pm | 12 | ||||
| -rw-r--r-- | lib/Travelynx/Controller/Passengerrights.pm | 13 | ||||
| -rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 109 | ||||
| -rwxr-xr-x | lib/Travelynx/Model/Journeys.pm | 751 | ||||
| -rw-r--r-- | templates/add_journey.html.ep | 2 | ||||
| -rw-r--r-- | templates/landingpage.html.ep | 2 | 
7 files changed, 870 insertions, 722 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; diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm index 8af57be..6ffd310 100755 --- a/lib/Travelynx/Controller/Api.pm +++ b/lib/Travelynx/Controller/Api.pm @@ -492,18 +492,18 @@ sub import_v1 {  	my $tx = $db->begin;  	$opt{db} = $db; -	my ( $journey_id, $error ) = $self->add_journey(%opt); +	my ( $journey_id, $error ) = $self->journeys->add(%opt);  	my $journey;  	if ( not $error ) { -		$journey = $self->get_journey( +		$journey = $self->journeys->get_single(  			uid        => $uid,  			db         => $db,  			journey_id => $journey_id,  			verbose    => 1  		);  		$error -		  = $self->journey_sanity_check( $journey, $payload->{lax} ? 1 : 0 ); +		  = $self->journeys->sanity_check( $journey, $payload->{lax} ? 1 : 0 );  	}  	if ($error) { @@ -526,7 +526,11 @@ sub import_v1 {  		);  	}  	else { -		$self->invalidate_stats_cache( $opt{rt_departure}, $db, $uid ); +		$self->journeys->invalidate_stats_cache( +			ts  => $opt{rt_departure}, +			db  => $db, +			uid => $uid +		);  		$tx->commit;  		$self->render(  			json => { diff --git a/lib/Travelynx/Controller/Passengerrights.pm b/lib/Travelynx/Controller/Passengerrights.pm index 7d9a00b..dc73f37 100644 --- a/lib/Travelynx/Controller/Passengerrights.pm +++ b/lib/Travelynx/Controller/Passengerrights.pm @@ -50,7 +50,8 @@ sub mark_if_missed_connection {  sub mark_substitute_connection {  	my ( $self, $journey ) = @_; -	my @substitute_candidates = reverse $self->get_user_travels( +	my @substitute_candidates = reverse $self->journeys->get( +		uid    => $self->current_user->{id},  		after  => $journey->{sched_departure}->clone->subtract( hours => 1 ),  		before => $journey->{sched_departure}->clone->add( hours => 12 ),  		with_datetime => 1, @@ -87,7 +88,8 @@ sub list_candidates {  	my $now         = DateTime->now( time_zone => 'Europe/Berlin' );  	my $range_start = $now->clone->subtract( months => 6 ); -	my @journeys = $self->get_user_travels( +	my @journeys = $self->journeys->get( +		uid           => $self->current_user->{id},  		after         => $range_start,  		before        => $now,  		with_datetime => 1, @@ -112,7 +114,8 @@ sub list_candidates {  	@journeys = grep { $_->{delay} >= 60 or $_->{connection_missed} } @journeys; -	my @cancelled = $self->get_user_travels( +	my @cancelled = $self->journeys->get( +		uid           => $self->current_user->{id},  		after         => $range_start,  		before        => $now,  		cancelled     => 1, @@ -163,7 +166,7 @@ sub generate {  		return;  	} -	my $journey = $self->get_journey( +	my $journey = $self->journeys->get_single(  		uid           => $uid,  		journey_id    => $journey_id,  		verbose       => 1, @@ -187,7 +190,7 @@ sub generate {  		$self->mark_substitute_connection($journey);  	}  	elsif ( $journey->{delay} < 120 ) { -		my @connections = $self->get_user_travels( +		my @connections = $self->journey->get(  			uid           => $uid,  			after         => $journey->{rt_arrival},  			before        => $journey->{rt_arrival}->clone->add( hours => 2 ), diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index e33009f..59bad45 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -60,14 +60,14 @@ sub user_status {  	  )  	{  		for my $candidate ( -			$self->get_user_travels( +			$self->journeys->get(  				uid   => $user->{id},  				limit => 10,  			)  		  )  		{  			if ( $candidate->{sched_dep_ts} eq $ts ) { -				$journey = $self->get_journey( +				$journey = $self->journeys->get_single(  					uid           => $user->{id},  					journey_id    => $candidate->{id},  					verbose       => 1, @@ -389,8 +389,12 @@ sub log_action {  		}  	}  	elsif ( $params->{action} eq 'delete' ) { -		my $error = $self->delete_journey( $params->{id}, $params->{checkin}, -			$params->{checkout} ); +		my $error = $self->journeys->delete( +			uid      => $self->current_user->{id}, +			id       => $params->{id}, +			checkin  => $params->{checkin}, +			checkout => $params->{checkout} +		);  		if ($error) {  			$self->render(  				json => { @@ -474,7 +478,8 @@ sub redirect_to_station {  sub cancelled {  	my ($self) = @_; -	my @journeys = $self->get_user_travels( +	my @journeys = $self->journeys->get( +		uid           => $self->current_user->{id},  		cancelled     => 1,  		with_datetime => 1  	); @@ -523,7 +528,8 @@ sub commute {  	);  	my $interval_end = $interval_start->clone->add( years => 1 ); -	my @journeys = $self->get_user_travels( +	my @journeys = $self->journeys->get( +		uid           => $self->current_user->{id},  		after         => $interval_start,  		before        => $interval_end,  		with_datetime => 1, @@ -616,7 +622,10 @@ sub map_history {  	my $route_type    = $self->param('route_type');  	my $with_polyline = $route_type eq 'beeline' ? 0 : 1; -	my @journeys = $self->get_user_travels( with_polyline => $with_polyline ); +	my @journeys = $self->journeys->get( +		uid           => $self->current_user->{id}, +		with_polyline => $with_polyline +	);  	if ( not @journeys ) {  		$self->render( @@ -647,7 +656,8 @@ sub map_history {  sub json_history {  	my ($self) = @_; -	$self->render( json => [ $self->get_user_travels ] ); +	$self->render( +		json => [ $self->journeys->get( uid => $self->current_user->{id} ) ] );  }  sub csv_history { @@ -669,7 +679,13 @@ sub csv_history {  	);  	$buf .= $csv->string; -	for my $journey ( $self->get_user_travels( with_datetime => 1 ) ) { +	for my $journey ( +		$self->journeys->get( +			uid           => $self->current_user->{id}, +			with_datetime => 1 +		) +	  ) +	{  		if (  			$csv->combine(  				$journey->{type}, @@ -708,7 +724,10 @@ sub yearly_history {  	# -> Limit time range to avoid accidental DoS.  	if ( not( $year =~ m{ ^ [0-9]{4} $ }x and $year > 1990 and $year < 2100 ) )  	{ -		@journeys = $self->get_user_travels( with_datetime => 1 ); +		@journeys = $self->journeys->get( +			uid           => $self->current_user->{id}, +			with_datetime => 1 +		);  	}  	else {  		my $interval_start = DateTime->new( @@ -721,7 +740,8 @@ sub yearly_history {  			second    => 0,  		);  		my $interval_end = $interval_start->clone->add( years => 1 ); -		@journeys = $self->get_user_travels( +		@journeys = $self->journeys->get( +			uid           => $self->current_user->{id},  			after         => $interval_start,  			before        => $interval_end,  			with_datetime => 1 @@ -766,7 +786,10 @@ sub monthly_history {  			and $month < 13 )  	  )  	{ -		@journeys = $self->get_user_travels( with_datetime => 1 ); +		@journeys = $self->journeys->get( +			uid           => $self->current_user->{id}, +			with_datetime => 1 +		);  	}  	else {  		my $interval_start = DateTime->new( @@ -779,7 +802,8 @@ sub monthly_history {  			second    => 0,  		);  		my $interval_end = $interval_start->clone->add( months => 1 ); -		@journeys = $self->get_user_travels( +		@journeys = $self->journeys->get( +			uid           => $self->current_user->{id},  			after         => $interval_start,  			before        => $interval_end,  			with_datetime => 1 @@ -827,7 +851,7 @@ sub journey_details {  		return;  	} -	my $journey = $self->get_journey( +	my $journey = $self->journeys->get_single(  		uid           => $uid,  		journey_id    => $journey_id,  		verbose       => 1, @@ -919,7 +943,7 @@ sub edit_journey {  		return;  	} -	my $journey = $self->get_journey( +	my $journey = $self->journeys->get_single(  		uid           => $uid,  		journey_id    => $journey_id,  		verbose       => 1, @@ -952,8 +976,12 @@ sub edit_journey {  		{  			my $datetime = $parser->parse_datetime( $self->param($key) );  			if ( $datetime and $datetime->epoch ne $journey->{$key}->epoch ) { -				$error = $self->update_journey_part( $db, $journey->{id}, -					$key, $datetime ); +				$error = $self->journeys->update( +					uid  => $uid, +					db   => $db, +					id   => $journey->{id}, +					$key => $datetime +				);  				if ($error) {  					last;  				} @@ -963,8 +991,12 @@ sub edit_journey {  			if ( defined $self->param($key)  				and $self->param($key) ne $journey->{$key} )  			{ -				$error = $self->update_journey_part( $db, $journey->{id}, $key, -					$self->param($key) ); +				$error = $self->journeys->update( +					uid  => $uid, +					db   => $db, +					id   => $journey->{id}, +					$key => $self->param($key) +				);  				if ($error) {  					last;  				} @@ -977,8 +1009,12 @@ sub edit_journey {  					or $journey->{user_data}{$key} ne $self->param($key) )  			  )  			{ -				$error = $self->update_journey_part( $db, $journey->{id}, $key, -					$self->param($key) ); +				$error = $self->journeys->update( +					uid  => $uid, +					db   => $db, +					id   => $journey->{id}, +					$key => $self->param($key) +				);  				if ($error) {  					last;  				} @@ -989,30 +1025,36 @@ sub edit_journey {  			my @route_new = split( qr{\r?\n\r?}, $self->param('route') );  			@route_new = grep { $_ ne '' } @route_new;  			if ( join( '|', @route_old ) ne join( '|', @route_new ) ) { -				$error -				  = $self->update_journey_part( $db, $journey->{id}, 'route', -					[@route_new] ); +				$error = $self->journeys->update( +					uid   => $uid, +					db    => $db, +					id    => $journey->{id}, +					route => [@route_new] +				);  			}  		}  		{  			my $cancelled_old = $journey->{cancelled}     // 0;  			my $cancelled_new = $self->param('cancelled') // 0;  			if ( $cancelled_old != $cancelled_new ) { -				$error -				  = $self->update_journey_part( $db, $journey->{id}, -					'cancelled', $cancelled_new ); +				$error = $self->journeys->update( +					uid       => $uid, +					db        => $db, +					id        => $journey->{id}, +					cancelled => $cancelled_new +				);  			}  		}  		if ( not $error ) { -			$journey = $self->get_journey( +			$journey = $self->journeys->get_single(  				uid           => $uid,  				db            => $db,  				journey_id    => $journey_id,  				verbose       => 1,  				with_datetime => 1,  			); -			$error = $self->journey_sanity_check($journey); +			$error = $self->journeys->sanity_check($journey);  		}  		if ( not $error ) {  			$tx->commit; @@ -1108,18 +1150,19 @@ sub add_journey_form {  		my $db = $self->pg->db;  		my $tx = $db->begin; -		$opt{db} = $db; +		$opt{db}  = $db; +		$opt{uid} = $self->current_user->{id}; -		my ( $journey_id, $error ) = $self->add_journey(%opt); +		my ( $journey_id, $error ) = $self->journeys->add(%opt);  		if ( not $error ) { -			my $journey = $self->get_journey( +			my $journey = $self->journeys->get_single(  				uid        => $self->current_user->{id},  				db         => $db,  				journey_id => $journey_id,  				verbose    => 1  			); -			$error = $self->journey_sanity_check($journey); +			$error = $self->journeys->sanity_check($journey);  		}  		if ($error) { diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm new file mode 100755 index 0000000..868b1bf --- /dev/null +++ b/lib/Travelynx/Model/Journeys.pm @@ -0,0 +1,751 @@ +package Travelynx::Model::Journeys; + +use Geo::Distance; +use List::MoreUtils qw(after_incl before_incl); +use Travel::Status::DE::IRIS::Stations; + +use strict; +use warnings; +use 5.020; + +use DateTime; +use JSON; + +sub epoch_to_dt { +	my ($epoch) = @_; + +	# Bugs (and user errors) may lead to undefined timestamps. Set them to +	# 1970-01-01 to avoid crashing and show obviously wrong data instead. +	$epoch //= 0; + +	return DateTime->from_epoch( +		epoch     => $epoch, +		time_zone => 'Europe/Berlin', +		locale    => 'de-DE', +	); +} + +sub get_station { +	my ( $station_name, $exact_match ) = @_; + +	my @candidates +	  = Travel::Status::DE::IRIS::Stations::get_station($station_name); + +	if ( @candidates == 1 ) { +		if ( not $exact_match ) { +			return $candidates[0]; +		} +		if (   $candidates[0][0] eq $station_name +			or $candidates[0][1] eq $station_name +			or $candidates[0][2] eq $station_name ) +		{ +			return $candidates[0]; +		} +		return undef; +	} +	return undef; +} + +sub grep_unknown_stations { +	my (@stations) = @_; + +	my @unknown_stations; +	for my $station (@stations) { +		my $station_info = get_station($station); +		if ( not $station_info ) { +			push( @unknown_stations, $station ); +		} +	} +	return @unknown_stations; +} + +sub new { +	my ( $class, %opt ) = @_; + +	$opt{journey_edit_mask} = { +		sched_departure => 0x0001, +		real_departure  => 0x0002, +		from_station    => 0x0004, +		route           => 0x0010, +		is_cancelled    => 0x0020, +		sched_arrival   => 0x0100, +		real_arrival    => 0x0200, +		to_station      => 0x0400, +	}; + +	return bless( \%opt, $class ); +} + +# Returns (journey id, error) +# Must be called during a transaction. +# Must perform a rollback on error. +sub add { +	my ( $self, %opt ) = @_; + +	my $db          = $opt{db}; +	my $uid         = $opt{uid}; +	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( +			ts  => $opt{rt_departure}, +			db  => $db, +			uid => $uid +		); +	}; + +	if ($@) { +		$self->{log}->error("add_journey($uid): $@"); +		return ( undef, 'add_journey failed: ' . $@ ); +	} + +	return ( $journey_id, undef ); +} + +sub update { +	my ( $self, %opt ) = @_; + +	my $db         = $opt{db} // $self->{pg}->db; +	my $uid        = $opt{uid}; +	my $journey_id = $opt{id}; + +	my $rows; + +	my $journey = $self->get_single( +		uid           => $uid, +		db            => $db, +		journey_id    => $journey_id, +		with_datetime => 1, +	); + +	eval { +		if ( exists $opt{from_name} ) { +			my $from_station = get_station( $opt{from_name}, 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; +		} +		if ( exists $opt{to_name} ) { +			my $to_station = get_station( $opt{to_name}, 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; +		} +		if ( exists $opt{sched_departure} ) { +			$rows = $db->update( +				'journeys', +				{ +					sched_departure => $opt{sched_departure}, +					edited          => $journey->{edited} | 0x0001, +				}, +				{ +					id => $journey_id, +				} +			)->rows; +		} +		if ( exists $opt{rt_departure} ) { +			$rows = $db->update( +				'journeys', +				{ +					real_departure => $opt{rt_departure}, +					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( +				ts  => $opt{rt_departure}, +				db  => $db, +				uid => $uid, +			); +		} +		if ( exists $opt{sched_arrival} ) { +			$rows = $db->update( +				'journeys', +				{ +					sched_arrival => $opt{sched_arrival}, +					edited        => $journey->{edited} | 0x0100, +				}, +				{ +					id => $journey_id, +				} +			)->rows; +		} +		if ( exists $opt{rt_arrival} ) { +			$rows = $db->update( +				'journeys', +				{ +					real_arrival => $opt{rt_arrival}, +					edited       => $journey->{edited} | 0x0200, +				}, +				{ +					id => $journey_id, +				} +			)->rows; +		} +		if ( exists $opt{route} ) { +			my @new_route = map { [ $_, {}, undef ] } @{ $opt{route} }; +			$rows = $db->update( +				'journeys', +				{ +					route  => JSON->new->encode( \@new_route ), +					edited => $journey->{edited} | 0x0010, +				}, +				{ +					id => $journey_id, +				} +			)->rows; +		} +		if ( exists $opt{cancelled} ) { +			$rows = $db->update( +				'journeys', +				{ +					cancelled => $opt{cancelled}, +					edited    => $journey->{edited} | 0x0020, +				}, +				{ +					id => $journey_id, +				} +			)->rows; +		} +		if ( exists $opt{comment} ) { +			$journey->{user_data}{comment} = $opt{comment}; +			$rows = $db->update( +				'journeys', +				{ +					user_data => JSON->new->encode( $journey->{user_data} ), +				}, +				{ +					id => $journey_id, +				} +			)->rows; +		} +		if ( not defined $rows ) { +			die("Invalid update key\n"); +		} +	}; + +	if ($@) { +		$self->{log}->error("update($journey_id): $@"); +		return "update($journey_id): $@"; +	} +	if ( $rows == 1 ) { +		$self->invalidate_stats_cache( +			ts  => $journey->{rt_departure}, +			db  => $db, +			uid => $uid, +		); +		return undef; +	} +	return "update($journey_id): did not match any journey part"; +} + +sub delete { +	my ( $self, %opt ) = @_; + +	my $uid            = $opt{uid}; +	my $db             = $opt{db} // $self->{pg}->db; +	my $journey_id     = $opt{id}; +	my $checkin_epoch  = $opt{checkin}; +	my $checkout_epoch = $opt{checkout}; + +	my @journeys = $self->get( +		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 = $db->delete( +			'journeys', +			{ +				user_id => $uid, +				id      => $journey_id, +			} +		)->rows; +	}; + +	if ($@) { +		$self->{log}->error("Delete($uid, $journey_id): $@"); +		return 'DELETE failed: ' . $@; +	} + +	if ( $rows == 1 ) { +		$self->invalidate_stats_cache( +			ts  => epoch_to_dt( $journey->{rt_dep_ts} ), +			uid => $uid +		); +		return undef; +	} +	return sprintf( 'Deleted %d rows, expected 1', $rows ); +} + +sub get { +	my ( $self, %opt ) = @_; + +	my $uid = $opt{uid}; + +	# If get 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->{station_by_eva}->{ $ref->{from_eva} } ) { +			$ref->{from_ds100} = $station->[0]; +			$ref->{from_name}  = $station->[1]; +		} +		if ( my $station = $self->{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->{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; +} + +sub get_single { +	my ( $self, %opt ) = @_; + +	$opt{cancelled} = 'any'; +	my @journeys = $self->get(%opt); +	if ( @journeys == 0 ) { +		return undef; +	} + +	return $journeys[0]; +} + +sub get_oldest_ts { +	my ( $self, %opt ) = @_; +	my $uid = $opt{uid}; +	my $db  = $opt{db} // $self->{pg}->db; + +	my $res_h = $db->select( +		'journeys_str', +		['sched_dep_ts'], +		{ +			user_id => $uid, +		}, +		{ +			limit    => 1, +			order_by => { +				-asc => 'real_dep_ts', +			}, +		} +	)->hash; + +	if ($res_h) { +		return epoch_to_dt( $res_h->{sched_dep_ts} ); +	} +	return undef; +} + +sub sanity_check { +	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 +		  = grep_unknown_stations( map { $_->[0] } @{ $journey->{route} } ); +		if (@unknown_stations) { +			return 'Unbekannte Station(en): ' . join( ', ', @unknown_stations ); +		} +	} + +	return undef; +} + +sub get_travel_distance { +	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 ); +} + +# 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. +sub invalidate_stats_cache { +	my ( $self, %opt ) = @_; + +	my $ts  = $opt{ts}; +	my $db  = $opt{db} // $self->{pg}->db; +	my $uid = $opt{uid}; + +	$db->delete( +		'journey_stats', +		{ +			user_id => $uid, +			year    => $ts->year, +			month   => $ts->month, +		} +	); +	$db->delete( +		'journey_stats', +		{ +			user_id => $uid, +			year    => $ts->year, +			month   => 0, +		} +	); +} + +1; diff --git a/templates/add_journey.html.ep b/templates/add_journey.html.ep index 0682fa9..777fdee 100644 --- a/templates/add_journey.html.ep +++ b/templates/add_journey.html.ep @@ -1,5 +1,5 @@  <h1>Zugfahrt eingeben</h1> -% if (not get_oldest_journey_ts()) { +% if (not journeys->get_oldest_ts()) {  	<div class="row">  		<div class="col s12">  			<div class="card info-color"> diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep index ce817e7..0534c71 100644 --- a/templates/landingpage.html.ep +++ b/templates/landingpage.html.ep @@ -70,7 +70,7 @@  		</div>  	</div>  	<h1>Letzte Fahrten</h1> -	%= include '_history_trains', date_format => '%d.%m', journeys => [get_user_travels(limit => 5, with_datetime => 1)]; +	%= include '_history_trains', date_format => '%d.%m', journeys => [journeys->get(uid => current_user()->{id}, limit => 5, with_datetime => 1)];  % }  % else {  	<div class="row"> | 
