Unverified Commit c250a2f2 authored by Birte Kristina Friesel's avatar Birte Kristina Friesel
Browse files

Add experimental EFA support

Squashed commit of the following:

commit b7457791
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Sun Jun 15 08:18:46 2025 +0200

    changelog

commit 7f3d6106
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Sat Jun 14 19:55:51 2025 +0200

    Mark EFA backends as experimental for now

    For instance, VRR has very interesting issues when checking into departures
    that do not have real-time data yet.

commit 3370c0f6
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Sat Jun 14 19:49:48 2025 +0200

    InTransit: remove debug output

commit deb5444f
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Sat Jun 14 19:12:44 2025 +0200

    frontend js for checked-in view: never show fractional delays

commit d47ff961
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Sat Jun 14 19:12:31 2025 +0200

    worker: add EFA support

commit 3a955c01
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Sat Jun 14 17:48:46 2025 +0200

    EFA: checkin support

    worker support and cancellations are still missing

commit 19dea1ad
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Sat Jun 14 14:32:59 2025 +0200

    actions.js: pass on efa parameter

commit 8f18ff2c
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Sat Jun 14 14:32:48 2025 +0200

    EFA: implement geolocation lookup

commit bce1139b
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Sat Jun 14 14:32:21 2025 +0200

    EFA: ->id is no longer supported, use ->id_num

commit e4397e6b
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Mon Jun 9 20:34:22 2025 +0200

    ... derp

commit e0c4cbf8
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Mon Jun 9 18:28:35 2025 +0200

    database: it's dbris, not ris

commit bfb1e834
Merge: 42f9a00d f1da50f9
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Mon Jun 9 18:20:51 2025 +0200

    Merge branch 'main' into efa-support

commit 42f9a00d
Author: Birte Kristina Friesel <derf@finalrewind.org>
Date:   Wed Jan 8 18:11:28 2025 +0100

    EFA support (WiP)
parent f1da50f9
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@ requires 'Mojolicious::Plugin::OAuth2';
requires 'Mojo::Pg';
requires 'Text::CSV';
requires 'Text::Markdown';
requires 'Travel::Status::DE::EFA';
requires 'Travel::Status::MOTIS', '>= 0.01';
requires 'Travel::Status::DE::DBRIS', '>= 0.10';
requires 'Travel::Status::DE::HAFAS', '>= 6.20';
+162 −8
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@ use List::MoreUtils qw(first_index);
use Travel::Status::DE::DBRIS::Formation;
use Travelynx::Helper::DBDB;
use Travelynx::Helper::DBRIS;
use Travelynx::Helper::EFA;
use Travelynx::Helper::HAFAS;
use Travelynx::Helper::IRIS;
use Travelynx::Helper::MOTIS;
@@ -160,11 +161,12 @@ sub startup {
		cache_iris_main => sub {
			my ($self) = @_;

			return Cache::File->new(
			state $cache = Cache::File->new(
				cache_root      => $self->app->config->{cache}->{schedule},
				default_expires => '6 hours',
				lock_level      => Cache::File::LOCK_LOCAL(),
			);
			return $cache;
		}
	);

@@ -172,11 +174,12 @@ sub startup {
		cache_iris_rt => sub {
			my ($self) = @_;

			return Cache::File->new(
			state $cache = Cache::File->new(
				cache_root      => $self->app->config->{cache}->{realtime},
				default_expires => '70 seconds',
				lock_level      => Cache::File::LOCK_LOCAL(),
			);
			return $cache;
		}
	);

@@ -194,7 +197,7 @@ sub startup {

	$self->attr(
		renamed_station => sub {
			my $legacy_to_new = JSON->new->utf8->decode(
			state $legacy_to_new = JSON->new->utf8->decode(
				scalar read_file('share/old_station_names.json') );
			return $legacy_to_new;
		}
@@ -219,6 +222,20 @@ sub startup {
		}
	);

	$self->helper(
		efa => sub {
			my ($self) = @_;
			state $efa = Travelynx::Helper::EFA->new(
				log            => $self->app->log,
				main_cache     => $self->app->cache_iris_main,
				realtime_cache => $self->app->cache_iris_rt,
				root_url       => $self->base_url_for('/')->to_abs,
				user_agent     => $self->ua,
				version        => $self->app->config->{version},
			);
		}
	);

	$self->helper(
		dbris => sub {
			my ($self) = @_;
@@ -490,15 +507,18 @@ sub startup {
				return Mojo::Promise->reject('You are already checked in');
			}

			if ( $opt{motis} ) {
				return $self->_checkin_motis_p(%opt);
			}
			if ( $opt{dbris} ) {
				return $self->_checkin_dbris_p(%opt);
			}
			if ( $opt{efa} ) {
				return $self->_checkin_efa_p(%opt);
			}
			if ( $opt{hafas} ) {
				return $self->_checkin_hafas_p(%opt);
			}
			if ( $opt{motis} ) {
				return $self->_checkin_motis_p(%opt);
			}

			my $promise = Mojo::Promise->new;

@@ -868,6 +888,137 @@ sub startup {
		}
	);

	$self->helper(
		'_checkin_efa_p' => sub {
			my ( $self, %opt ) = @_;
			my $station = $opt{station};
			my $trip_id = $opt{train_id};
			my $ts      = $opt{ts};
			my $uid     = $opt{uid} // $self->current_user->{id};
			my $db      = $opt{db}  // $self->pg->db;

			my $promise = Mojo::Promise->new;
			$self->efa->get_journey_p(
				service => $opt{efa},
				trip_id => $trip_id
			)->then(
				sub {
					my ($journey) = @_;

					my $found;
					for my $stop ( $journey->route ) {
						if ( $stop->id_num == $station ) {
							$found = $stop;

							# Lines may serve the same stop several times.
							# Keep looking until the scheduled departure
							# matches the one passed while checking in.
							if ( $ts and $stop->sched_dep->epoch == $ts ) {
								last;
							}
						}
					}
					if ( not $found ) {
						$promise->reject(
"Did not find stop '$station' within journey '$trip_id'"
						);
						return;
					}

					for my $stop ( $journey->route ) {
						$self->stations->add_or_update(
							stop => $stop,
							db   => $db,
							efa  => $opt{efa},
						);
					}

					eval {
						$self->in_transit->add(
							uid        => $uid,
							db         => $db,
							journey    => $journey,
							stop       => $found,
							trip_id    => $trip_id,
							backend_id => $self->stations->get_backend_id(
								efa => $opt{efa}
							),
						);
					};
					if ($@) {
						$self->app->log->error(
							"Checkin($uid): INSERT failed: $@");
						$promise->reject( 'INSERT failed: ' . $@ );
						return;
					}

					my $polyline;
					if ( $journey->polyline ) {
						my @station_list;
						my @coordinate_list;
						for my $coord ( $journey->polyline ) {
							if ( $coord->{stop} ) {
								push(
									@coordinate_list,
									[
										$coord->{lon}, $coord->{lat},
										$coord->{stop}->id_num
									]
								);
								push( @station_list,
									$coord->{stop}->full_name );
							}
							else {
								push( @coordinate_list,
									[ $coord->{lon}, $coord->{lat} ] );
							}
						}

						# equal length → polyline only consists of straight
						# lines between stops. that's not helpful.
						if ( @station_list == @coordinate_list ) {
							$self->log->debug( 'Ignoring polyline for '
								  . $journey->line
								  . ' as it only consists of straight lines between stops.'
							);
						}
						else {
							$polyline = {
								from_eva => ( $journey->route )[0]->id_num,
								to_eva   => ( $journey->route )[-1]->id_num,
								coords   => \@coordinate_list,
							};
						}
					}

					if ($polyline) {
						$self->in_transit->set_polyline(
							uid      => $uid,
							db       => $db,
							polyline => $polyline,
						);
					}

					# mustn't be called during a transaction
					if ( not $opt{in_transaction} ) {
						$self->run_hook( $uid, 'checkin' );
					}

					$promise->resolve($journey);

					return;
				}
			)->catch(
				sub {
					my ($err) = @_;
					$promise->reject($err);
					return;
				}
			)->wait;
			return $promise;
		}
	);

	$self->helper(
		'_checkin_hafas_p' => sub {
			my ( $self, %opt ) = @_;
@@ -877,7 +1028,6 @@ sub startup {
			my $ts       = $opt{ts};
			my $uid      = $opt{uid} // $self->current_user->{id};
			my $db       = $opt{db}  // $self->pg->db;
			my $hafas;

			my $promise = Mojo::Promise->new;

@@ -1136,7 +1286,11 @@ sub startup {
				return $promise->resolve( 0, 'race condition' );
			}

			if ( $user->{is_dbris} or $user->{is_hafas} or $user->{is_motis} ) {
			if (   $user->{is_dbris}
				or $user->{is_efa}
				or $user->{is_hafas}
				or $user->{is_motis} )
			{
				return $self->_checkout_journey_p(%opt);
			}

+63 −6
Original line number Diff line number Diff line
@@ -10,6 +10,7 @@ use DateTime;
use File::Slurp qw(read_file);
use List::Util  qw();
use JSON;
use Travel::Status::DE::EFA;
use Travel::Status::DE::HAFAS;
use Travel::Status::DE::IRIS::Stations;
use Travel::Status::MOTIS;
@@ -3023,6 +3024,19 @@ qq{select distinct checkout_station_id from in_transit where backend_id = 0;}
			}
		);
	},

	# v62 -> v63
	# Add EFA backend support
	sub {
		my ($db) = @_;
		$db->query(
			qq{
				alter table schema_version add column efa varchar(12);
				update schema_version set version = 63;
				update schema_version set efa = '0';
			}
		);
	},
);

sub sync_stations {
@@ -3213,6 +3227,37 @@ sub sync_stations {
	}
}

sub sync_backends_efa {
	my ($db) = @_;
	for my $service ( Travel::Status::DE::EFA::get_services() ) {
		my $present = $db->select(
			'backends',
			'count(*) as count',
			{
				efa  => 1,
				name => $service->{shortname}
			}
		)->hash->{count};
		if ( not $present ) {
			$db->insert(
				'backends',
				{
					dbris => 0,
					efa   => 1,
					hafas => 0,
					iris  => 0,
					motis => 0,
					name  => $service->{shortname},
				},
				{ on_conflict => undef }
			);
		}
	}

	$db->update( 'schema_version',
		{ efa => $Travel::Status::DE::EFA::VERSION } );
}

sub sync_backends_hafas {
	my ($db) = @_;
	for my $service ( Travel::Status::DE::HAFAS::get_services() ) {
@@ -3228,10 +3273,11 @@ sub sync_backends_hafas {
			$db->insert(
				'backends',
				{
					iris  => 0,
					hafas => 1,
					efa   => 0,
					dbris => 0,
					efa   => 0,
					hafas => 1,
					iris  => 0,
					motis => 0,
					name  => $service->{shortname},
				},
				{ on_conflict => undef }
@@ -3258,10 +3304,10 @@ sub sync_backends_motis {
			$db->insert(
				'backends',
				{
					iris  => 0,
					hafas => 0,
					efa   => 0,
					dbris => 0,
					efa   => 0,
					hafas => 0,
					iris  => 0,
					motis => 1,
					name  => $service->{shortname},
				},
@@ -3361,6 +3407,17 @@ sub migrate_db {
		}
	}

	my $efa_version = get_schema_version( $db, 'efa' );
	say "Found backend table for EFA v${efa_version}";
	if ( $efa_version eq $Travel::Status::DE::EFA::VERSION ) {
		say 'Backend table is up-to-date';
	}
	else {
		say
"Synchronizing with Travel::Status::DE::EFA $Travel::Status::DE::EFA::VERSION";
		sync_backends_efa($db);
	}

	my $hafas_version = get_schema_version( $db, 'hafas' );
	say "Found backend table for HAFAS v${hafas_version}";
	if ( $hafas_version eq $Travel::Status::DE::HAFAS::VERSION ) {
+79 −0
Original line number Diff line number Diff line
@@ -185,6 +185,85 @@ sub run {
			next;
		}

		if ( $entry->{is_efa} ) {
			eval {
				$self->app->efa->get_journey_p(
					trip_id => $train_id,
					service => $entry->{backend_name}
				)->then(
					sub {
						my ($journey) = @_;

						my $found_dep;
						my $found_arr;
						for my $stop ( $journey->route ) {
							if ( $stop->id_num == $dep ) {
								$found_dep = $stop;
							}
							if ( $arr and $stop->id_num == $arr ) {
								$found_arr = $stop;
								last;
							}
						}
						if ( not $found_dep ) {
							$self->app->log->debug(
								"Did not find $dep within journey $train_id");
							return;
						}

						if ( $found_dep->rt_dep ) {
							$self->app->in_transit->update_departure_efa(
								uid     => $uid,
								journey => $journey,
								stop    => $found_dep,
								dep_eva => $dep,
								arr_eva => $arr,
								trip_id => $train_id,
							);
						}

						if ( $found_arr and $found_arr->rt_arr ) {
							$self->app->in_transit->update_arrival_efa(
								uid     => $uid,
								journey => $journey,
								stop    => $found_arr,
								dep_eva => $dep,
								arr_eva => $arr,
								trip_id => $train_id,
							);
						}
					}
				)->catch(
					sub {
						my ($err) = @_;
						$backend_issues += 1;
						$self->app->log->error(
"work($uid) @ EFA $entry->{backend_name}: journey: $err"
						);
					}
				)->wait;

				if (    $arr
					and $entry->{real_arr_ts}
					and $now->epoch - $entry->{real_arr_ts} > 600 )
				{
					$self->app->checkout_p(
						station => $arr,
						force   => 2,
						dep_eva => $dep,
						arr_eva => $arr,
						uid     => $uid
					)->wait;
				}
			};
			if ($@) {
				$errors += 1;
				$self->app->log->error(
					"work($uid) @ EFA $entry->{backend_name}: $@");
			}
			next;
		}

		if ( $entry->{is_motis} ) {

			eval {
+43 −0
Original line number Diff line number Diff line
@@ -1077,6 +1077,49 @@ sub backend_form {
			$backend->{homepage}    = 'https://www.bahn.de';
			$backend->{recommended} = 1;
		}
		elsif ( $backend->{efa} ) {
			if ( my $s = $self->efa->get_service( $backend->{name} ) ) {
				$type                = 'EFA';
				$backend->{longname} = $s->{name};
				$backend->{homepage} = $s->{homepage};
				$backend->{regions}  = [ map { $place_map{$_} // $_ }
					  @{ $s->{coverage}{regions} // [] } ];
				$backend->{has_area}     = $s->{coverage}{area} ? 1 : 0;
				$backend->{experimental} = 1;

				if (
					    $s->{coverage}{area}
					and $s->{coverage}{area}{type} eq 'Polygon'
					and $self->lonlat_in_polygon(
						$s->{coverage}{area}{coordinates},
						[ $user_lon, $user_lat ]
					)
				  )
				{
					push( @suggested_backends, $backend );
				}
				elsif ( $s->{coverage}{area}
					and $s->{coverage}{area}{type} eq 'MultiPolygon' )
				{
					for my $s_poly (
						@{ $s->{coverage}{area}{coordinates} // [] } )
					{
						if (
							$self->lonlat_in_polygon(
								$s_poly, [ $user_lon, $user_lat ]
							)
						  )
						{
							push( @suggested_backends, $backend );
							last;
						}
					}
				}
			}
			else {
				$type = undef;
			}
		}
		elsif ( $backend->{hafas} ) {

			# These backends lack a journey endpoint or are no longer
Loading