diff options
Diffstat (limited to 'lib/Travelynx')
| -rw-r--r-- | lib/Travelynx/Command/database.pm | 239 | ||||
| -rwxr-xr-x | lib/Travelynx/Controller/Api.pm | 26 | ||||
| -rw-r--r-- | lib/Travelynx/Helper/IRIS.pm | 1 | ||||
| -rwxr-xr-x | lib/Travelynx/Model/Journeys.pm | 116 | ||||
| -rw-r--r-- | lib/Travelynx/Model/Stations.pm | 88 | 
5 files changed, 371 insertions, 99 deletions
| diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index 33612c3..d3a5006 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -6,12 +6,27 @@ package Travelynx::Command::database;  use Mojo::Base 'Mojolicious::Command';  use DateTime; +use File::Slurp qw(read_file); +use JSON;  use Travel::Status::DE::IRIS::Stations;  has description => 'Initialize or upgrade database layout';  has usage => sub { shift->extract_usage }; +sub get_iris_version { +	my ($db) = @_; +	my $version; + +	eval { $version = $db->select( 'schema_version', ['iris'] )->hash->{iris}; }; +	if ($@) { + +		# If it failed, the version table does not exist -> run setup first. +		return undef; +	} +	return $version; +} +  sub get_schema_version {  	my ($db) = @_;  	my $version; @@ -1106,8 +1121,212 @@ my @migrations = (  			}  		);  	}, + +	# v26 -> v27 +	# add list of stations that are not (or no longer) present in T-S-DE-IRIS +	# (in this case, stations that were removed up to 1.74) +	sub { +		my ($db) = @_; +		$db->query( +			qq{ +				alter table schema_version +					add column iris varchar(12); +				create table stations ( +					eva int not null primary key, +					ds100 varchar(16) not null, +					name varchar(64) not null, +					lat real not null, +					lon real not null, +					source smallint not null, +					archived bool not null +				); +				update schema_version set version = 27; +				update schema_version set iris = '0'; +			} +		); +	},  ); +sub sync_stations { +	my ( $db, $iris_version ) = @_; + +	$db->update( 'schema_version', +		{ iris => $Travel::Status::DE::IRIS::Stations::VERSION } ); + +	say 'Updating stations table, this may take a while ...'; +	my $total = scalar Travel::Status::DE::IRIS::Stations::get_stations(); +	my $count = 0; +	for my $s ( Travel::Status::DE::IRIS::Stations::get_stations() ) { +		my ( $ds100, $name, $eva, $lon, $lat ) = @{$s}; +		$db->insert( +			'stations', +			{ +				eva      => $eva, +				ds100    => $ds100, +				name     => $name, +				lat      => $lat, +				lon      => $lon, +				source   => 0, +				archived => 0 +			}, +			{ +				on_conflict => \ +				  '(eva) do update set archived = false, source = 0' +			} +		); +		if ( $count++ % 1000 == 0 ) { +			printf( "    %2.0f%% complete\n", $count * 100 / $total ); +		} +	} +	say '    done'; + +	my $res1 = $db->query( +		qq{ +			select checkin_station_id +			from journeys +			left join stations on journeys.checkin_station_id = stations.eva +			where stations.eva is null +			limit 1; +		} +	)->hash; + +	my $res2 = $db->query( +		qq{ +			select checkout_station_id +			from journeys +			left join stations on journeys.checkout_station_id = stations.eva +			where stations.eva is null +			limit 1; +		} +	)->hash; + +	if ( $res1 or $res2 ) { +		say 'Dropping stats cache for archived stations ...'; +		$db->query('truncate journey_stats;'); +	} + +	say 'Updating archived stations ...'; +	my $old_stations +	  = JSON->new->utf8->decode( scalar read_file('share/old_stations.json') ); +	for my $s ( @{$old_stations} ) { +		$db->insert( +			'stations', +			{ +				eva      => $s->{eva}, +				ds100    => $s->{ds100}, +				name     => $s->{name}, +				lat      => $s->{latlong}[0], +				lon      => $s->{latlong}[1], +				source   => 0, +				archived => 1 +			}, +			{ on_conflict => undef } +		); +	} + +	if ( $iris_version == 0 ) { +		say 'Applying EVA ID changes ...'; +		for my $change ( +			[ 721394, 301002, 'RKBP: Kronenplatz (U), Karlsruhe' ], +			[ +				721356, 901012, +				'RKME: Ettlinger Tor/Staatstheater (U), Karlsruhe' +			], +		  ) +		{ +			my ( $old, $new, $desc ) = @{$change}; +			my $rows = $db->update( +				'journeys', +				{ checkout_station_id => $new }, +				{ checkout_station_id => $old } +			)->rows; +			$rows += $db->update( +				'journeys', +				{ checkin_station_id => $new }, +				{ checkin_station_id => $old } +			)->rows; +			if ($rows) { +				say "$desc ($old -> $new) : $rows rows"; +			} +		} +	} + +	say 'Checking for unknown EVA IDs ...'; +	my $found = 0; + +	$res1 = $db->query( +		qq{ +			select checkin_station_id +			from journeys +			left join stations on journeys.checkin_station_id = stations.eva +			where stations.eva is null; +		} +	); + +	$res2 = $db->query( +		qq{ +			select checkout_station_id +			from journeys +			left join stations on journeys.checkout_station_id = stations.eva +			where stations.eva is null; +		} +	); + +	my %notified; +	while ( my $row = $res1->hash ) { +		my $eva = $row->{checkin_station_id}; +		if ( not $found ) { +			$found = 1; +			say ''; +			say '------------8<----------'; +			say 'Travel::Status::DE::IRIS v' +			  . $Travel::Status::DE::IRIS::Stations::VERSION; +		} +		if ( not $notified{$eva} ) { +			say $eva; +			$notified{$eva} = 1; +		} +	} + +	while ( my $row = $res2->hash ) { +		my $eva = $row->{checkout_station_id}; +		if ( not $found ) { +			$found = 1; +			say ''; +			say '------------8<----------'; +			say 'Travel::Status::DE::IRIS v' +			  . $Travel::Status::DE::IRIS::Stations::VERSION; +		} +		if ( not $notified{$eva} ) { +			say $eva; +			$notified{$eva} = 1; +		} +	} + +	if ($found) { +		say '------------8<----------'; +		say ''; +		say +'Due to a conceptual flaw in past travelynx releases, your database contains unknown EVA IDs.'; +		say +'Please file a bug report titled "Missing EVA IDs after DB migration" at https://github.com/derf/travelynx/issues'; +		say 'and include the list shown above in the bug report.'; +		say +'If you do not have a GitHub account, please send an E-Mail to derf+travelynx@finalrewind.org instead.'; +		say ''; +		say 'This issue does not affect usability or long-term data integrity,'; +		say 'and handling it is not time-critical.'; +		say +'Past journeys referencing unknown EVA IDs may have inaccurate distance statistics,'; +		say +'but this will be resolved once a future release handles those EVA IDs.'; +		say 'Note that this issue was already present in previous releases.'; +	} +	else { +		say 'None found.'; +	} +} +  sub setup_db {  	my ($db) = @_;  	my $tx = $db->begin; @@ -1129,7 +1348,7 @@ sub migrate_db {  	say "Found travelynx schema v${schema_version}";  	if ( $schema_version == @migrations ) { -		say "Database layout is up-to-date"; +		say 'Database layout is up-to-date';  	}  	eval { @@ -1144,6 +1363,24 @@ sub migrate_db {  		exit(1);  	} +	my $iris_version = get_iris_version($db); +	say "Found IRIS station database v${iris_version}"; +	if ( $iris_version eq $Travel::Status::DE::IRIS::Stations::VERSION ) { +		say 'Station database is up-to-date'; +	} +	else { +		eval { +			say +"Synchronizing with Travel::Status::DE::IRIS $Travel::Status::DE::IRIS::Stations::VERSION"; +			sync_stations( $db, $iris_version ); +		}; +		if ($@) { +			say STDERR "Synchronization failed: $@"; +			say STDERR "Rolling back to v${schema_version}"; +			exit(1); +		} +	} +  	if ( get_schema_version($db) == @migrations ) {  		$tx->commit;  	} diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm index 3e9c5eb..f686222 100755 --- a/lib/Travelynx/Controller/Api.pm +++ b/lib/Travelynx/Controller/Api.pm @@ -7,7 +7,6 @@ use Mojo::Base 'Mojolicious::Controller';  use DateTime;  use List::Util; -use Travel::Status::DE::IRIS::Stations;  use UUID::Tiny qw(:std);  # Internal Helpers @@ -184,41 +183,24 @@ sub travel_v1 {  			return;  		} -		if ( -			@{ -				[ -					Travel::Status::DE::IRIS::Stations::get_station( -						$from_station) -				] -			} != 1 -		  ) -		{ +		if ( not $self->stations->search($from_station) ) {  			$self->render(  				json => {  					success    => \0,  					deprecated => \0, -					error      => 'fromStation is ambiguous', +					error      => 'Unknown fromStation',  					status     => $self->get_user_status_json_v1($uid)  				},  			);  			return;  		} -		if ( -			$to_station -			and @{ -				[ -					Travel::Status::DE::IRIS::Stations::get_station( -						$to_station) -				] -			} != 1 -		  ) -		{ +		if ( $to_station and not $self->stations->search($to_station) ) {  			$self->render(  				json => {  					success    => \0,  					deprecated => \0, -					error      => 'toStation is ambiguous', +					error      => 'Unknown toStation',  					status     => $self->get_user_status_json_v1($uid)  				},  			); diff --git a/lib/Travelynx/Helper/IRIS.pm b/lib/Travelynx/Helper/IRIS.pm index ef179c8..3222dad 100644 --- a/lib/Travelynx/Helper/IRIS.pm +++ b/lib/Travelynx/Helper/IRIS.pm @@ -13,6 +13,7 @@ use utf8;  use Mojo::Promise;  use Mojo::UserAgent;  use Travel::Status::DE::IRIS; +use Travel::Status::DE::IRIS::Stations;  sub new {  	my ( $class, %opt ) = @_; diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index a8ca299..3706916 100755 --- a/lib/Travelynx/Model/Journeys.pm +++ b/lib/Travelynx/Model/Journeys.pm @@ -6,7 +6,6 @@ package Travelynx::Model::Journeys;  use GIS::Distance;  use List::MoreUtils qw(after_incl before_incl); -use Travel::Status::DE::IRIS::Stations;  use strict;  use warnings; @@ -35,33 +34,12 @@ sub epoch_to_dt {  	);  } -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 ( $self, @stations ) = @_;  	my @unknown_stations;  	for my $station (@stations) { -		my $station_info = get_station($station); +		my $station_info = $self->{stations}->get_by_name($station);  		if ( not $station_info ) {  			push( @unknown_stations, $station );  		} @@ -100,8 +78,8 @@ sub add {  	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} ); +	my $dep_station = $self->{stations}->search( $opt{dep_station} ); +	my $arr_station = $self->{stations}->search( $opt{arr_station} );  	if ( not $dep_station ) {  		return ( undef, 'Unbekannter Startbahnhof' ); @@ -134,10 +112,14 @@ sub add {  	my $route_has_stop  = 0;  	for my $station ( @{ $opt{route} || [] } ) { -		if ( $station eq $dep_station->[1] or $station eq $dep_station->[0] ) { +		if (   $station eq $dep_station->{name} +			or $station eq $dep_station->{ds100} ) +		{  			$route_has_start = 1;  		} -		if ( $station eq $arr_station->[1] or $station eq $arr_station->[0] ) { +		if (   $station eq $arr_station->{name} +			or $station eq $arr_station->{ds100} ) +		{  			$route_has_stop = 1;  		}  	} @@ -145,15 +127,15 @@ sub add {  	my @route;  	if ( not $route_has_start ) { -		push( @route, [ $dep_station->[1], {}, undef ] ); +		push( @route, [ $dep_station->{name}, {}, undef ] );  	}  	if ( $opt{route} ) {  		my @unknown_stations;  		for my $station ( @{ $opt{route} } ) { -			my $station_info = get_station($station); +			my $station_info = $self->{stations}->search($station);  			if ($station_info) { -				push( @route, [ $station_info->[1], {}, undef ] ); +				push( @route, [ $station_info->{name}, {}, undef ] );  			}  			else {  				push( @route,            [ $station, {}, undef ] ); @@ -175,7 +157,7 @@ sub add {  	}  	if ( not $route_has_stop ) { -		push( @route, [ $arr_station->[1], {}, undef ] ); +		push( @route, [ $arr_station->{name}, {}, undef ] );  	}  	my $entry = { @@ -184,11 +166,11 @@ sub add {  		train_line          => $opt{train_line},  		train_no            => $opt{train_no},  		train_id            => 'manual', -		checkin_station_id  => $dep_station->[2], +		checkin_station_id  => $dep_station->{eva},  		checkin_time        => $now,  		sched_departure     => $opt{sched_departure},  		real_departure      => $opt{rt_departure}, -		checkout_station_id => $arr_station->[2], +		checkout_station_id => $arr_station->{eva},  		sched_arrival       => $opt{sched_arrival},  		real_arrival        => $opt{rt_arrival},  		checkout_time       => $now, @@ -252,14 +234,14 @@ sub update {  	eval {  		if ( exists $opt{from_name} ) { -			my $from_station = get_station( $opt{from_name}, 1 ); +			my $from_station = $self->{stations}->search( $opt{from_name} );  			if ( not $from_station ) {  				die("Unbekannter Startbahnhof\n");  			}  			$rows = $db->update(  				'journeys',  				{ -					checkin_station_id => $from_station->[2], +					checkin_station_id => $from_station->{eva},  					edited             => $journey->{edited} | 0x0004,  				},  				{ @@ -268,14 +250,14 @@ sub update {  			)->rows;  		}  		if ( exists $opt{to_name} ) { -			my $to_station = get_station( $opt{to_name}, 1 ); +			my $to_station = $self->{stations}->search( $opt{to_name} );  			if ( not $to_station ) {  				die("Unbekannter Zielbahnhof\n");  			}  			$rows = $db->update(  				'journeys',  				{ -					checkout_station_id => $to_station->[2], +					checkout_station_id => $to_station->{eva},  					edited              => $journey->{edited} | 0x0400,  				},  				{ @@ -559,13 +541,13 @@ sub get {  			$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->{stations}->get_by_eva( $ref->{from_eva} ) ) { +			$ref->{from_ds100} = $station->{ds100}; +			$ref->{from_name}  = $station->{name};  		} -		if ( my $station = $self->{station_by_eva}->{ $ref->{to_eva} } ) { -			$ref->{to_ds100} = $station->[0]; -			$ref->{to_name}  = $station->[1]; +		if ( my $station = $self->{stations}->get_by_eva( $ref->{to_eva} ) ) { +			$ref->{to_ds100} = $station->{ds100}; +			$ref->{to_name}  = $station->{name};  		}  		if ( $opt{with_datetime} ) { @@ -938,7 +920,8 @@ sub sanity_check {  	}  	if ( $journey->{edited} & 0x0010 and not $lax ) {  		my @unknown_stations -		  = grep_unknown_stations( map { $_->[0] } @{ $journey->{route} } ); +		  = $self->grep_unknown_stations( map { $_->[0] } +			  @{ $journey->{route} } );  		if (@unknown_stations) {  			return 'Unbekannte Station(en): ' . join( ', ', @unknown_stations );  		} @@ -989,8 +972,6 @@ sub get_travel_distance {  	my $prev_station = shift @polyline;  	for my $station (@polyline) { - -		#lonlatlonlat  		$distance_polyline += $geo->distance_metal(  			$prev_station->[1], $prev_station->[0],  			$station->[1],      $station->[0] @@ -998,45 +979,30 @@ sub get_travel_distance {  		$prev_station = $station;  	} -	$prev_station = get_station( shift @route ); +	$prev_station = $self->{stations}->get_by_name( 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_metal( -					$prev_station->[4], $prev_station->[3], -					$station->[4],      $station->[3] -				); -			} -			else { -				$skipped++; -			} +		if ( my $station = $self->{stations}->get_by_name($station_name) ) { +			$from_station_beeline //= $prev_station; +			$to_station_beeline = $station; +			$distance_intermediate += $geo->distance_metal( +				$prev_station->{lat}, $prev_station->{lon}, +				$station->{lat},      $station->{lon} +			);  			$prev_station = $station;  		}  	}  	if ( $from_station_beeline and $to_station_beeline ) {  		$distance_beeline = $geo->distance_metal( -			$from_station_beeline->[4], $from_station_beeline->[3], -			$to_station_beeline->[4],   $to_station_beeline->[3] +			$from_station_beeline->{lat}, $from_station_beeline->{lon}, +			$to_station_beeline->{lat},   $to_station_beeline->{lon}  		);  	} @@ -1264,10 +1230,8 @@ sub get_connection_targets {  	my @destinations  	  = $res->hashes->grep( sub { shift->{count} >= $min_count } )  	  ->map( sub { shift->{dest} } )->each; -	@destinations -	  = grep { $self->{station_by_eva}{$_} } @destinations; -	@destinations -	  = map { $self->{station_by_eva}{$_}->[1] } @destinations; +	@destinations = $self->{stations}->get_by_evas(@destinations); +	@destinations = map { $_->{name} } @destinations;  	return @destinations;  } diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm new file mode 100644 index 0000000..6c898b1 --- /dev/null +++ b/lib/Travelynx/Model/Stations.pm @@ -0,0 +1,88 @@ +package Travelynx::Model::Stations; + +# Copyright (C) 2022 Daniel Friesel +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +use strict; +use warnings; +use 5.020; + +sub new { +	my ( $class, %opt ) = @_; + +	return bless( \%opt, $class ); +} + +# Fast +sub get_by_eva { +	my ( $self, $eva, %opt ) = @_; + +	if ( not $eva ) { +		return; +	} + +	my $db = $opt{db} // $self->{pg}->db; + +	return $db->select( 'stations', '*', { eva => $eva } )->hash; +} + +# Fast +sub get_by_evas { +	my ( $self, @evas ) = @_; + +	my @ret +	  = $self->{pg}->db->select( 'stations', '*', { eva => { '=', \@evas } } ) +	  ->hashes->each; +	return @ret; +} + +# Slow +sub get_latlon_by_name { +	my ( $self, %opt ) = @_; + +	my $db = $opt{db} // $self->{pg}->db; + +	my %location; +	my $res = $db->select( 'stations', [ 'name', 'lat', 'lon' ] ); +	while ( my $row = $res->hash ) { +		$location{ $row->{name} } = [ $row->{lat}, $row->{lon} ]; +	} +	return \%location; +} + +# Slow +sub get_by_name { +	my ( $self, $name, %opt ) = @_; + +	my $db = $opt{db} // $self->{pg}->db; + +	return $db->select( 'stations', '*', { name => $name }, { limit => 1 } ) +	  ->hash; +} + +# Slow +sub get_by_ds100 { +	my ( $self, $ds100, %opt ) = @_; + +	my $db = $opt{db} // $self->{pg}->db; + +	return $db->select( 'stations', '*', { ds100 => $ds100 }, { limit => 1 } ) +	  ->hash; +} + +# Can be slow +sub search { +	my ( $self, $identifier, %opt ) = @_; + +	if ( $identifier =~ m{ ^ \d+ $ }x ) { +		return $self->get_by_eva( $identifier, %opt ) +		  // $self->get_by_ds100( $identifier, %opt ) +		  // $self->get_by_name( $identifier, %opt ); +	} + +	return $self->get_by_ds100( $identifier, %opt ) +	  // $self->get_by_name( $identifier, %opt ); +} + +1; | 
