diff options
| -rw-r--r-- | lib/Travelynx/Command/database.pm | 23 | ||||
| -rw-r--r-- | lib/Travelynx/Command/maintenance.pm | 1 | ||||
| -rw-r--r-- | lib/Travelynx/Controller/Account.pm | 32 | ||||
| -rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 285 | ||||
| -rw-r--r-- | lib/Travelynx/Model/Users.pm | 27 | ||||
| -rw-r--r-- | templates/_checked_in.html.ep | 7 | ||||
| -rw-r--r-- | templates/_transit_fyi.html.ep | 19 | ||||
| -rw-r--r-- | templates/account.html.ep | 2 | ||||
| -rw-r--r-- | templates/use_history.html.ep | 24 | 
9 files changed, 293 insertions, 127 deletions
| diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index d580c05..33612c3 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -1083,6 +1083,29 @@ my @migrations = (  			}  		);  	}, + +	# v25 -> v26 +	# travelynx 1.24 adds local transit connections and needs to know targets +	# for that to work, as local transit does not support checkins yet. +	sub { +		my ($db) = @_; +		$db->query( +			qq{ +				create table localtransit ( +					user_id integer not null references users (id) primary key, +					data jsonb +				); +				create view user_transit as select +					id, +					use_history, +					localtransit.data as data +					from users +					left join localtransit on localtransit.user_id = id +				; +				update schema_version set version = 26; +			} +		); +	},  );  sub setup_db { diff --git a/lib/Travelynx/Command/maintenance.pm b/lib/Travelynx/Command/maintenance.pm index d6380fd..60667b7 100644 --- a/lib/Travelynx/Command/maintenance.pm +++ b/lib/Travelynx/Command/maintenance.pm @@ -149,6 +149,7 @@ sub run {  		my $transit_res  = $db->delete( 'in_transit',    { user_id => $uid } );  		my $hooks_res    = $db->delete( 'webhooks',      { user_id => $uid } );  		my $trwl_res     = $db->delete( 'traewelling',   { user_id => $uid } ); +		my $lt_res       = $db->delete( 'localtransit',  { user_id => $uid } );  		my $password_res  		  = $db->delete( 'pending_passwords', { user_id => $uid } );  		my $user_res = $db->delete( 'users', { id => $uid } ); diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index d55b470..12b179b 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -6,7 +6,7 @@ package Travelynx::Controller::Account;  use Mojo::Base 'Mojolicious::Controller';  use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64); -use UUID::Tiny qw(:std); +use UUID::Tiny                 qw(:std);  # Internal Helpers @@ -498,8 +498,11 @@ sub privacy {  sub insight {  	my ($self) = @_; -	my $user        = $self->current_user; -	my $use_history = $self->users->use_history( uid => $user->{id} ); +	my $user = $self->current_user; +	my ( $use_history, $destinations ) = $self->users->use_history( +		uid                => $user->{id}, +		with_local_transit => 1 +	);  	if ( $self->param('action') and $self->param('action') eq 'save' ) {  		if ( $self->param('on_departure') ) { @@ -516,16 +519,31 @@ sub insight {  			$use_history &= ~0x02;  		} +		if ( $self->param('local_transit') ) { +			$use_history |= 0x04; +		} +		else { +			$use_history &= ~0x04; +		} + +		if ( $self->param('destinations') ) { +			$destinations +			  = [ split( qr{\r?\n\r?}, $self->param('destinations') ) ]; +		} +  		$self->users->use_history( -			uid => $user->{id}, -			set => $use_history +			uid          => $user->{id}, +			set          => $use_history, +			destinations => $destinations  		);  		$self->flash( success => 'use_history' );  		$self->redirect_to('account');  	} -	$self->param( on_departure => $use_history & 0x01 ? 1 : 0 ); -	$self->param( on_arrival   => $use_history & 0x02 ? 1 : 0 ); +	$self->param( on_departure  => $use_history & 0x01 ? 1 : 0 ); +	$self->param( on_arrival    => $use_history & 0x02 ? 1 : 0 ); +	$self->param( local_transit => $use_history & 0x04 ? 1 : 0 ); +	$self->param( destinations  => join( "\n", @{$destinations} ) );  	$self->render('use_history');  } diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index bdf57e2..a4fe72b 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -28,8 +28,11 @@ sub has_str_in_list {  sub get_connecting_trains_p {  	my ( $self, %opt ) = @_; -	my $uid         = $opt{uid} //= $self->current_user->{id}; -	my $use_history = $self->users->use_history( uid => $uid ); +	my $uid = $opt{uid} //= $self->current_user->{id}; +	my ( $use_history, $lt_stops ) = $self->users->use_history( +		uid                => $uid, +		with_local_transit => 1 +	);  	my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );  	my $now = $self->now->epoch; @@ -72,7 +75,7 @@ sub get_connecting_trains_p {  		@destinations = grep { $_ ne $exclude_via } @destinations;  	} -	if ( not @destinations ) { +	if ( not( @destinations or $use_history & 0x04 and @{$lt_stops} ) ) {  		return $promise->reject;  	} @@ -82,44 +85,45 @@ sub get_connecting_trains_p {  	my $iris_promise = Mojo::Promise->new; -	$self->iris->get_departures_p( -		station      => $eva, -		lookbehind   => 10, -		lookahead    => $lookahead, -		with_related => 1 -	)->then( -		sub { -			my ($stationboard) = @_; -			if ( $stationboard->{errstr} ) { -				$iris_promise->reject( $stationboard->{errstr} ); -				return; -			} - -			@{ $stationboard->{results} } = map { $_->[0] } -			  sort { $a->[1] <=> $b->[1] } -			  map  { [ $_, $_->departure ? $_->departure->epoch : 0 ] } -			  @{ $stationboard->{results} }; -			my @results; -			my @cancellations; -			my $excluded_train; -			my %via_count = map { $_ => 0 } @destinations; -			for my $train ( @{ $stationboard->{results} } ) { -				if ( not $train->departure ) { -					next; -				} -				if (    $exclude_before -					and $train->departure -					and $train->departure->epoch < $exclude_before ) -				{ -					next; -				} -				if (    $exclude_train_id -					and $train->train_id eq $exclude_train_id ) -				{ -					$excluded_train = $train; -					next; +	if (@destinations) { +		$self->iris->get_departures_p( +			station      => $eva, +			lookbehind   => 10, +			lookahead    => $lookahead, +			with_related => 1 +		)->then( +			sub { +				my ($stationboard) = @_; +				if ( $stationboard->{errstr} ) { +					$iris_promise->reject( $stationboard->{errstr} ); +					return;  				} +				@{ $stationboard->{results} } = map { $_->[0] } +				  sort { $a->[1] <=> $b->[1] } +				  map  { [ $_, $_->departure ? $_->departure->epoch : 0 ] } +				  @{ $stationboard->{results} }; +				my @results; +				my @cancellations; +				my $excluded_train; +				my %via_count = map { $_ => 0 } @destinations; +				for my $train ( @{ $stationboard->{results} } ) { +					if ( not $train->departure ) { +						next; +					} +					if (    $exclude_before +						and $train->departure +						and $train->departure->epoch < $exclude_before ) +					{ +						next; +					} +					if (    $exclude_train_id +						and $train->train_id eq $exclude_train_id ) +					{ +						$excluded_train = $train; +						next; +					} +               # In general, this function is meant to return feasible               # connections. However, cancelled connections may also be of               # interest and are also useful for logging cancellations. @@ -136,97 +140,105 @@ sub get_connecting_trains_p {               # the route. Also note that this specific case is not yet handled               # properly by the cancellation logic etc. -				if ( $train->departure_is_cancelled ) { -					my @via -					  = ( $train->sched_route_post, $train->sched_route_end ); -					for my $dest (@destinations) { -						if ( has_str_in_list( $dest, @via ) ) { -							push( @cancellations, [ $train, $dest ] ); -							next; +					if ( $train->departure_is_cancelled ) { +						my @via = ( +							$train->sched_route_post, $train->sched_route_end +						); +						for my $dest (@destinations) { +							if ( has_str_in_list( $dest, @via ) ) { +								push( @cancellations, [ $train, $dest ] ); +								next; +							}  						}  					} -				} -				else { -					my @via = ( $train->route_post, $train->route_end ); -					for my $dest (@destinations) { -						if ( $via_count{$dest} < 2 -							and has_str_in_list( $dest, @via ) ) -						{ -							push( @results, [ $train, $dest ] ); +					else { +						my @via = ( $train->route_post, $train->route_end ); +						for my $dest (@destinations) { +							if ( $via_count{$dest} < 2 +								and has_str_in_list( $dest, @via ) ) +							{ +								push( @results, [ $train, $dest ] );                   # Show all past and up to two future departures per destination -							if ( not $train->departure -								or $train->departure->epoch >= $now ) -							{ -								$via_count{$dest}++; +								if ( not $train->departure +									or $train->departure->epoch >= $now ) +								{ +									$via_count{$dest}++; +								} +								next;  							} -							next;  						}  					}  				} -			} - -			@results = map { $_->[0] } -			  sort { $a->[1] <=> $b->[1] } -			  map { -				[ -					$_, -					$_->[0]->departure->epoch -					  // $_->[0]->sched_departure->epoch -				] -			  } @results; -			@cancellations = map { $_->[0] } -			  sort { $a->[1] <=> $b->[1] } -			  map { [ $_, $_->[0]->sched_departure->epoch ] } @cancellations; - -			# remove trains whose route matches the excluded one's -			if ($excluded_train) { -				my $route_pre = join( '|', reverse $excluded_train->route_pre ); -				@results -				  = grep { join( '|', $_->[0]->route_post ) ne $route_pre } -				  @results; -				my $route_post = join( '|', $excluded_train->route_post ); -				@results -				  = grep { join( '|', $_->[0]->route_post ) ne $route_post } -				  @results; -			} -			# add message IDs and 'transfer short' hints -			for my $result (@results) { -				my $train = $result->[0]; -				my @message_ids -				  = List::Util::uniq map { $_->[1] } $train->raw_messages; -				$train->{message_id} = { map { $_ => 1 } @message_ids }; -				my $interchange_duration; -				if ( exists $stationinfo->{i} ) { -					$interchange_duration -					  = $stationinfo->{i}{$arr_platform}{ $train->platform }; -					$interchange_duration //= $stationinfo->{i}{"*"}; +				@results = map { $_->[0] } +				  sort { $a->[1] <=> $b->[1] } +				  map { +					[ +						$_, +						$_->[0]->departure->epoch +						  // $_->[0]->sched_departure->epoch +					] +				  } @results; +				@cancellations = map { $_->[0] } +				  sort { $a->[1] <=> $b->[1] } +				  map  { [ $_, $_->[0]->sched_departure->epoch ] } +				  @cancellations; + +				# remove trains whose route matches the excluded one's +				if ($excluded_train) { +					my $route_pre +					  = join( '|', reverse $excluded_train->route_pre ); +					@results +					  = grep { join( '|', $_->[0]->route_post ) ne $route_pre } +					  @results; +					my $route_post = join( '|', $excluded_train->route_post ); +					@results +					  = grep { join( '|', $_->[0]->route_post ) ne $route_post } +					  @results;  				} -				if ( defined $interchange_duration ) { -					my $interchange_time -					  = ( $train->departure->epoch - $arr_epoch ) / 60; -					if ( $interchange_time < $interchange_duration ) { -						$train->{interchange_text} = 'Anschluss knapp'; -						$train->{interchange_icon} = 'directions_run'; + +				# add message IDs and 'transfer short' hints +				for my $result (@results) { +					my $train = $result->[0]; +					my @message_ids +					  = List::Util::uniq map { $_->[1] } $train->raw_messages; +					$train->{message_id} = { map { $_ => 1 } @message_ids }; +					my $interchange_duration; +					if ( exists $stationinfo->{i} ) { +						$interchange_duration +						  = $stationinfo->{i}{$arr_platform} +						  { $train->platform }; +						$interchange_duration //= $stationinfo->{i}{"*"};  					} -					elsif ( $interchange_time == $interchange_duration ) { -						$train->{interchange_text} -						  = 'Anschluss könnte knapp werden'; -						$train->{interchange_icon} = 'directions_run'; +					if ( defined $interchange_duration ) { +						my $interchange_time +						  = ( $train->departure->epoch - $arr_epoch ) / 60; +						if ( $interchange_time < $interchange_duration ) { +							$train->{interchange_text} = 'Anschluss knapp'; +							$train->{interchange_icon} = 'directions_run'; +						} +						elsif ( $interchange_time == $interchange_duration ) { +							$train->{interchange_text} +							  = 'Anschluss könnte knapp werden'; +							$train->{interchange_icon} = 'directions_run'; +						}  					}  				} -			} -			$iris_promise->resolve( [ @results, @cancellations ] ); -			return; -		} -	)->catch( -		sub { -			$iris_promise->reject(@_); -			return; -		} -	)->wait; +				$iris_promise->resolve( [ @results, @cancellations ] ); +				return; +			} +		)->catch( +			sub { +				$iris_promise->reject(@_); +				return; +			} +		)->wait; +	} +	else { +		$iris_promise->resolve( [] ); +	}  	my $hafas_promise = Mojo::Promise->new;  	my $rest_api      = $self->config->{backend}{hafas_rest_api}; @@ -254,6 +266,7 @@ sub get_connecting_trains_p {  			my ( $iris, $hafas ) = @_;  			my @iris_trains  = @{ $iris->[0] };  			my @hafas_trains = @{ $hafas->[0] }; +			my @transit_fyi;  			my $strp = DateTime::Format::Strptime->new(  				pattern   => '%Y-%m-%dT%H:%M:%S%z', @@ -296,6 +309,44 @@ sub get_connecting_trains_p {  						}  					}  				} +				if ( $use_history & 0x04 and @{$lt_stops} ) { +					my %via_count = map { $_ => 0 } @{$lt_stops}; +					for my $hafas_train (@hafas_trains) { +						for +						  my $stop ( @{ $hafas_train->{nextStopovers} // [] } ) +						{ +							for my $dest ( @{$lt_stops} ) { +								if (    $stop->{stop}{name} +									and $stop->{stop}{name} eq $dest +									and $via_count{$dest} < 2 +									and $hafas_train->{when} ) +								{ +									my $departure = $strp->parse_datetime( +										$hafas_train->{when} ); +									my $arrival +									  = $strp->parse_datetime( +										$stop->{arrival} ); +									if ( $departure->epoch >= $exclude_before ) +									{ +										$via_count{$dest}++; +										push( +											@transit_fyi, +											[ +												{ +													line => +													  $hafas_train->{line} +													  {name}, +													departure => $departure, +												}, +												$dest, $arrival +											] +										); +									} +								} +							} +						} +					} +				}  			};  			if ($@) {  				$self->app->log->error( @@ -303,7 +354,7 @@ sub get_connecting_trains_p {  				);  			} -			$promise->resolve( \@iris_trains ); +			$promise->resolve( \@iris_trains, \@transit_fyi );  			return;  		}  	)->catch( diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index dbd2a00..c36fa6d 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -9,6 +9,7 @@ use warnings;  use 5.020;  use DateTime; +use JSON;  my @sb_templates = (  	undef, @@ -483,12 +484,34 @@ sub use_history {  	my $uid   = $opt{uid};  	my $value = $opt{set}; +	if ( $opt{destinations} ) { +		$db->insert( +			'localtransit', +			{ +				user_id => $uid, +				data    => +				  JSON->new->encode( { destinations => $opt{destinations} } ) +			}, +			{ on_conflict => \'(user_id) do update set data = EXCLUDED.data' } +		); +	} +  	if ($value) {  		$db->update( 'users', { use_history => $value }, { id => $uid } );  	}  	else { -		return $db->select( 'users', ['use_history'], { id => $uid } ) -		  ->hash->{use_history}; +		if ( $opt{with_local_transit} ) { +			my $res = $db->select( +				'user_transit', +				[ 'use_history', 'data' ], +				{ id => $uid } +			)->expand->hash; +			return ( $res->{use_history}, $res->{data}{destinations} // [] ); +		} +		else { +			return $db->select( 'users', ['use_history'], { id => $uid } ) +			  ->hash->{use_history}; +		}  	}  } diff --git a/templates/_checked_in.html.ep b/templates/_checked_in.html.ep index 040c5de..dcf9ddc 100644 --- a/templates/_checked_in.html.ep +++ b/templates/_checked_in.html.ep @@ -200,6 +200,13 @@  				% }  				%= include '_connections', connections => \@connections, checkin_from => $journey->{arrival_countdown} < 0 ? $journey->{arr_ds100} : undef;  			% } +			% if (my @transit_fyi = @{stash('transit_fyi') // []}) { +				<span class="card-title" style="margin-top: 2ex;">Nahverkehr</span> +				% if ($journey->{arrival_countdown} < 0) { +					<p>Nur zur Information – kein Checkin möglich.</p> +				% } +				%= include '_transit_fyi', transit_fyi => \@transit_fyi; +			% }  			% if (defined $journey->{arrival_countdown} and $journey->{arrival_countdown} <= 0) {  				<p style="margin-top: 2ex;">  					Der automatische Checkout erfolgt wegen gelegentlich veralteter diff --git a/templates/_transit_fyi.html.ep b/templates/_transit_fyi.html.ep new file mode 100644 index 0000000..42233e5 --- /dev/null +++ b/templates/_transit_fyi.html.ep @@ -0,0 +1,19 @@ +<table class="striped"><tbody> +	% for my $res (@{$transit_fyi}) { +		% my ($info, $via, $via_arr) = @{$res}; +		% $via_arr = $via_arr ? $via_arr->strftime('%H:%M') : q{}; +		<tr> +			<td> +				%= $info->{line} +				<br/> +				%= $info->{departure}->strftime('%H:%M') +				% if ($info->{departure_delay}) { +					%= sprintf('(%+d)', $info->{departure_delay}) +				% } +			</td> +			<td> +				<%= $via %><br/><%= $via_arr %> +			</td> +		</tr> +	% } +</tbody></table> diff --git a/templates/account.html.ep b/templates/account.html.ep index 7658237..418291c 100644 --- a/templates/account.html.ep +++ b/templates/account.html.ep @@ -61,7 +61,7 @@  				<th scope="row">Verbindungen</th>  				<td>  					<a href="/account/insight"><i class="material-icons">edit</i></a> -					% if ($use_history & 0x03) { +					% if ($use_history & 0x07) {  						Vorschläge aktiv  					% }  					% else { diff --git a/templates/use_history.html.ep b/templates/use_history.html.ep index e8e129f..1632b8b 100644 --- a/templates/use_history.html.ep +++ b/templates/use_history.html.ep @@ -48,6 +48,30 @@  		</div>  	</div>  	<div class="row"> +		<div class="input-field col s12"> +			<label> +				%= check_box local_transit => 1 +				<span>Nahverkehr</span> +			</label> +		</div> +	</div> +	<div class="row"> +		<div class="col s12"> +			Zeige beim Reisestatus zusätzlich Anschlussmöglichkeiten an den +			Nahverkehr. Diese dienen lediglich zur Information; ein Checkin ist +			nicht möglich. Es werden nur Anschlussmöglichkeiten zu Zielen +			angezeigt, die im folgenden Feld gelistet sind (ein Ziel pro +			Zeile, z.B. „Eichlinghofen H-Bahn, Dortmund“). Falls travelynx in +			Zukunft eine Möglichkeit für Checkins in Nahverkehrsmittel erhält, +			wird diese Liste ggf. gelöscht. +		</div> +	</div> +	<div class="row"> +		<div class="col s12"> +			%= text_area 'destinations', id => 'destinations', class => 'materialize-textarea' +		</div> +	</div> +	<div class="row">  		<div class="col s3 m3 l3">  		</div>  		<div class="col s6 m6 l6 center-align"> | 
