diff options
-rw-r--r-- | cpanfile | 1 | ||||
-rwxr-xr-x | lib/Travelynx.pm | 170 | ||||
-rw-r--r-- | lib/Travelynx/Command/database.pm | 69 | ||||
-rw-r--r-- | lib/Travelynx/Command/work.pm | 79 | ||||
-rw-r--r-- | lib/Travelynx/Controller/Account.pm | 43 | ||||
-rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 91 | ||||
-rw-r--r-- | lib/Travelynx/Helper/DBRIS.pm | 1 | ||||
-rw-r--r-- | lib/Travelynx/Helper/EFA.pm | 102 | ||||
-rw-r--r-- | lib/Travelynx/Helper/HAFAS.pm | 14 | ||||
-rw-r--r-- | lib/Travelynx/Model/InTransit.pm | 171 | ||||
-rwxr-xr-x | lib/Travelynx/Model/Journeys.pm | 5 | ||||
-rw-r--r-- | lib/Travelynx/Model/Stations.pm | 63 | ||||
-rw-r--r-- | lib/Travelynx/Model/Users.pm | 3 | ||||
-rw-r--r-- | public/static/js/travelynx-actions.js | 8 | ||||
-rw-r--r-- | templates/_departures_efa.html.ep | 54 | ||||
-rw-r--r-- | templates/changelog.html.ep | 15 | ||||
-rw-r--r-- | templates/departures.html.ep | 3 | ||||
-rw-r--r-- | templates/select_backend.html.ep | 15 |
18 files changed, 862 insertions, 45 deletions
@@ -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'; diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index f554d08..3d892ec 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -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; } @@ -220,6 +223,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) = @_; state $dbris = Travelynx::Helper::DBRIS->new( @@ -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; @@ -869,6 +889,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); } diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index e264c89..1385389 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -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 ) { diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm index 2b01cb2..5ea1810 100644 --- a/lib/Travelynx/Command/work.pm +++ b/lib/Travelynx/Command/work.pm @@ -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 { diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 033b270..0978c88 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -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 diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 0cfccb1..9826211 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -562,11 +562,14 @@ sub geolocation { return; } - my ( $dbris_service, $hafas_service, $motis_service ); + my ( $dbris_service, $efa_service, $hafas_service, $motis_service ); my $backend = $self->stations->get_backend( backend_id => $backend_id ); if ( $backend->{dbris} ) { $dbris_service = $backend->{name}; } + if ( $backend->{efa} ) { + $efa_service = $backend->{name}; + } elsif ( $backend->{hafas} ) { $hafas_service = $backend->{name}; } @@ -617,6 +620,50 @@ sub geolocation { )->wait; return; } + elsif ($efa_service) { + $self->render_later; + + Travel::Status::DE::EFA->new_p( + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + service => $efa_service, + coord => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($efa) = @_; + my @results = map { + { + name => $_->full_name, + eva => $_->id_code, + distance => 0, + efa => $efa_service, + } + } $efa->results; + if ( @results > 10 ) { + @results = @results[ 0 .. 9 ]; + } + $self->render( + json => { + candidates => [@results], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + return; + } elsif ($hafas_service) { $self->render_later; @@ -726,8 +773,6 @@ sub geolocation { lon => $_->[0][3], lat => $_->[0][4], distance => $_->[1], - dbris => 0, - hafas => 0, } } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon, $lat, 10 ); @@ -796,6 +841,7 @@ sub travel_action { sub { return $self->checkin_p( dbris => $params->{dbris}, + efa => $params->{efa}, hafas => $params->{hafas}, motis => $params->{motis}, station => $params->{station}, @@ -832,6 +878,9 @@ sub travel_action { if ( $status->{is_dbris} ) { $station_link .= '?dbris=' . $status->{backend_name}; } + elsif ( $status->{is_efa} ) { + $station_link .= '?efa=' . $status->{backend_name}; + } elsif ( $status->{is_hafas} ) { $station_link .= '?hafas=' . $status->{backend_name}; } @@ -871,6 +920,9 @@ sub travel_action { if ( $status->{is_dbris} ) { $station_link .= '?dbris=' . $status->{backend_name}; } + elsif ( $status->{is_efa} ) { + $station_link .= '?efa=' . $status->{backend_name}; + } elsif ( $status->{is_hafas} ) { $station_link .= '?hafas=' . $status->{backend_name}; } @@ -929,6 +981,12 @@ sub travel_action { . '?dbris=' . $status->{backend_name}; } + elsif ( $status->{is_efa} ) { + $redir + = '/s/' + . $status->{dep_eva} . '?efa=' + . $status->{backend_name}; + } elsif ( $status->{is_hafas} ) { $redir = '/s/' @@ -959,6 +1017,7 @@ sub travel_action { $self->render_later; $self->checkin_p( dbris => $params->{dbris}, + efa => $params->{efa}, hafas => $params->{hafas}, motis => $params->{motis}, station => $params->{station}, @@ -1091,6 +1150,8 @@ sub station { my $dbris_service = $self->param('dbris') // ( $user->{backend_dbris} ? $user->{backend_name} : undef ); + my $efa_service = $self->param('efa') + // ( $user->{backend_efa} ? $user->{backend_name} : undef ); my $hafas_service = $self->param('hafas') // ( $user->{backend_hafas} ? $user->{backend_name} : undef ); my $motis_service = $self->param('motis') @@ -1118,6 +1179,15 @@ sub station { lookbehind => 30, ); } + elsif ($efa_service) { + $promise = $self->efa->get_departures_p( + service => $efa_service, + name => $station, + timestamp => $timestamp, + lookbehind => 30, + lookahead => 30, + ); + } elsif ($hafas_service) { $promise = $self->hafas->get_departures_p( service => $hafas_service, @@ -1209,6 +1279,16 @@ sub station { related_stations => [], }; } + elsif ($efa_service) { + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->datetime->epoch ] } $status->results; + $status = { + station_eva => $status->stop->id_num, + station_name => $status->stop->full_name, + related_stations => [], + }; + } elsif ($motis_service) { @results = map { $_->[0] } sort { $b->[1] <=> $a->[1] } @@ -1268,12 +1348,14 @@ sub station { eva => $user_status->{cancellation}{dep_eva}, destination_name => $user_status->{cancellation}{arr_name}, + efa => $efa_service, hafas => $hafas_service, ); } else { $connections_p = $self->get_connecting_trains_p( eva => $status->{station_eva}, + efa => $efa_service, hafas => $hafas_service ); } @@ -1287,6 +1369,7 @@ sub station { 'departures', user => $user, dbris => $dbris_service, + efa => $efa_service, hafas => $hafas_service, motis => $motis_service, eva => $status->{station_eva}, @@ -1308,6 +1391,7 @@ sub station { 'departures', user => $user, dbris => $dbris_service, + efa => $efa_service, hafas => $hafas_service, motis => $motis_service, eva => $status->{station_eva}, @@ -1328,6 +1412,7 @@ sub station { 'departures', user => $user, dbris => $dbris_service, + efa => $efa_service, hafas => $hafas_service, motis => $motis_service, eva => $status->{station_eva}, diff --git a/lib/Travelynx/Helper/DBRIS.pm b/lib/Travelynx/Helper/DBRIS.pm index 0a46758..9ddaa5f 100644 --- a/lib/Travelynx/Helper/DBRIS.pm +++ b/lib/Travelynx/Helper/DBRIS.pm @@ -94,7 +94,6 @@ sub get_journey_p { my ( $self, %opt ) = @_; my $promise = Mojo::Promise->new; - my $now = DateTime->now( time_zone => 'Europe/Berlin' ); my $agent = $self->{user_agent}; my $proxy; diff --git a/lib/Travelynx/Helper/EFA.pm b/lib/Travelynx/Helper/EFA.pm new file mode 100644 index 0000000..ba11764 --- /dev/null +++ b/lib/Travelynx/Helper/EFA.pm @@ -0,0 +1,102 @@ +package Travelynx::Helper::EFA; + +# Copyright (C) 2024 Birte Kristina Friesel +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +use strict; +use warnings; +use 5.020; + +use Travel::Status::DE::EFA; + +sub new { + my ( $class, %opt ) = @_; + + my $version = $opt{version}; + + $opt{header} + = { 'User-Agent' => +"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx" + }; + + return bless( \%opt, $class ); +} + +sub get_service { + my ( $self, $service ) = @_; + + return Travel::Status::DE::EFA::get_service($service); +} + +sub get_departures_p { + my ( $self, %opt ) = @_; + + my $when = ( + $opt{timestamp} + ? $opt{timestamp}->clone + : DateTime->now( time_zone => 'Europe/Berlin' ) + )->subtract( minutes => $opt{lookbehind} ); + return Travel::Status::DE::EFA->new_p( + service => $opt{service}, + name => $opt{name}, + datetime => $when, + full_routes => 1, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => $self->{user_agent}->request_timeout(5), + ); +} + +sub get_journey_p { + my ( $self, %opt ) = @_; + + my $promise = Mojo::Promise->new; + my $agent = $self->{user_agent}; + my $stopseq; + + if ( $opt{trip_id} =~ m{ ^ ([^@]*) @ ([^@]*) [(] ([^)]*) [)] (.*) $ }x ) { + $stopseq = { + stateless => $1, + stop_id => $2, + date => $3, + key => $4 + }; + } + else { + return $promise->reject("Invalid trip_id: $opt{trip_id}"); + } + + Travel::Status::DE::EFA->new_p( + service => $opt{service}, + stopseq => $stopseq, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => $agent->request_timeout(10), + )->then( + sub { + my ($efa) = @_; + my $journey = $efa->result; + + if ($journey) { + $self->{log}->debug("get_journey_p($opt{trip_id}): success"); + $promise->resolve($journey); + return; + } + $self->{log}->debug("get_journey_p($opt{trip_id}): no journey"); + $promise->reject('no journey'); + return; + } + )->catch( + sub { + my ($err) = @_; + $self->{log}->debug("get_journey_p($opt{trip_id}): error $err"); + $promise->reject($err); + return; + } + )->wait; + + return $promise; +} + +1; diff --git a/lib/Travelynx/Helper/HAFAS.pm b/lib/Travelynx/Helper/HAFAS.pm index ebf44d2..c35dfdb 100644 --- a/lib/Travelynx/Helper/HAFAS.pm +++ b/lib/Travelynx/Helper/HAFAS.pm @@ -35,6 +35,20 @@ sub new { return bless( \%opt, $class ); } +sub class_to_product { + my ( $self, $hafas ) = @_; + + my $bits = $hafas->get_active_service->{productbits}; + my $ret; + + for my $i ( 0 .. $#{$bits} ) { + $ret->{ 2**$i } + = ref( $bits->[$i] ) eq 'ARRAY' ? $bits->[$i][0] : $bits->[$i]; + } + + return $ret; +} + sub get_service { my ( $self, $service ) = @_; diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm index cc943b3..470b45d 100644 --- a/lib/Travelynx/Model/InTransit.pm +++ b/lib/Travelynx/Model/InTransit.pm @@ -110,8 +110,6 @@ sub add { my $now = DateTime->now( time_zone => 'Europe/Berlin' ); if ($train) { - - # IRIS $db->insert( 'in_transit', { @@ -142,9 +140,60 @@ sub add { } ); } - elsif ( $journey and $stop and $journey->can('product') ) { - - # HAFAS + elsif ( $journey + and $stop + and ref($journey) eq 'Travel::Status::DE::EFA::Trip' ) + { + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $j_stop->full_name, + $j_stop->id_num, + { + sched_arr => _epoch( $j_stop->sched_arr ), + sched_dep => _epoch( $j_stop->sched_dep ), + rt_arr => _epoch( $j_stop->rt_arr ), + rt_dep => _epoch( $j_stop->rt_dep ), + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + efa_load => $j_stop->occupancy, + lat => $j_stop->latlon->[0], + lon => $j_stop->latlon->[1], + } + ] + ); + } + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => 0, # TODO + checkin_station_id => $stop->id_num, + checkin_time => $now, + dep_platform => $stop->platform, + train_type => $journey->type // q{}, + train_line => $journey->line, + train_no => $journey->number // q{}, + train_id => $opt{trip_id}, + sched_departure => $stop->sched_dep, + real_departure => $stop->rt_dep // $stop->sched_dep, + route => $json->encode( \@route ), + data => JSON->new->encode( + { + rt => $stop->rt_dep ? 1 : 0, + %{ $data // {} } + } + ), + backend_id => $backend_id, + } + ); + } + elsif ( $journey + and $stop + and ref($journey) eq 'Travel::Status::DE::HAFAS::Journey' ) + { my @route; my $product = $journey->product_at( $stop->loc->eva ) // $journey->product; @@ -198,9 +247,10 @@ sub add { } ); } - elsif ( $journey and $stop ) { - - # DBRIS + elsif ( $journey + and $stop + and ref($journey) eq 'Travel::Status::DE::DBRIS::Journey' ) + { my $number = $journey->train_no // $journey->number // $train_suffix; my $line; @@ -284,9 +334,10 @@ sub add { } ); } - elsif ( $journey and $stopover ) { - - # MOTIS + elsif ( $journey + and $stopover + and ref($journey) eq 'Travel::Status::MOTIS::Trip' ) + { my @route; for my $journey_stopover ( $journey->stopovers ) { push( @@ -340,7 +391,7 @@ sub add { ); } else { - die('neither train nor journey specified'); + die('invalid arguments / argument types passed to InTransit->add'); } } @@ -944,6 +995,41 @@ sub update_departure_dbris { ); } +sub update_departure_efa { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + if ( $stop->rt_dep ) { + $ephemeral_data->{rt} = 1; + } + + # selecting on user_id and train_no avoids a race condition if a user checks + # into a new train while we are fetching data for their previous journey. In + # this case, the new train would receive data from the previous journey. + $db->update( + 'in_transit', + { + data => $json->encode($ephemeral_data), + real_departure => $stop->rt_dep, + }, + { + user_id => $uid, + train_id => $opt{trip_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_departure_motis { my ( $self, %opt ) = @_; my $uid = $opt{uid}; @@ -1137,6 +1223,67 @@ sub update_arrival_dbris { ); } +sub update_arrival_efa { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h + = $db->select( 'in_transit', [ 'data', 'route' ], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + my $old_route = $res_h ? $res_h->{route} : []; + + if ( $stop->rt_arr ) { + $ephemeral_data->{rt} = 1; + } + + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $j_stop->full_name, + $j_stop->id_num, + { + sched_arr => _epoch( $j_stop->sched_arr ), + sched_dep => _epoch( $j_stop->sched_dep ), + rt_arr => _epoch( $j_stop->rt_arr ), + rt_dep => _epoch( $j_stop->rt_dep ), + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + efa_load => $j_stop->occupancy, + lat => $j_stop->latlon->[0], + lon => $j_stop->latlon->[1], + } + ] + ); + } + + # selecting on user_id and train_no avoids a race condition if a user checks + # into a new train while we are fetching data for their previous journey. In + # this case, the new train would receive data from the previous journey. + $db->update( + 'in_transit', + { + data => $json->encode($ephemeral_data), + real_arrival => $stop->rt_arr, + route => $json->encode( [@route] ), + }, + { + user_id => $uid, + train_id => $opt{trip_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_arrival_motis { my ( $self, %opt ) = @_; my $uid = $opt{uid}; diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index 8efbab2..1662787 100755 --- a/lib/Travelynx/Model/Journeys.pm +++ b/lib/Travelynx/Model/Journeys.pm @@ -1735,6 +1735,8 @@ sub compute_stats { sub get_stats { my ( $self, %opt ) = @_; + $self->{log}->debug("get_stats"); + if ( $opt{cancelled} ) { $self->{log} ->warn('get_journey_stats called with illegal option cancelled => 1'); @@ -1761,9 +1763,12 @@ sub get_stats { ) ) { + $self->{log}->debug("got cached journey stats for $year/$month"); return $stats; } + $self->{log}->debug("computing journey stats for $year/$month"); + my $interval_start = DateTime->new( time_zone => 'Europe/Berlin', year => 2000, diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm index 174b3b4..bf35d1a 100644 --- a/lib/Travelynx/Model/Stations.pm +++ b/lib/Travelynx/Model/Stations.pm @@ -23,12 +23,15 @@ sub get_backend_id { # special case return 0; } - if ( $opt{hafas} and $self->{backend_id}{hafas}{ $opt{hafas} } ) { - return $self->{backend_id}{hafas}{ $opt{hafas} }; - } if ( $opt{dbris} and $self->{backend_id}{dbris}{ $opt{dbris} } ) { return $self->{backend_id}{dbris}{ $opt{dbris} }; } + if ( $opt{efa} and $self->{backend_id}{efa}{ $opt{efa} } ) { + return $self->{backend_id}{efa}{ $opt{efa} }; + } + if ( $opt{hafas} and $self->{backend_id}{hafas}{ $opt{hafas} } ) { + return $self->{backend_id}{hafas}{ $opt{hafas} }; + } if ( $opt{motis} and $self->{backend_id}{motis}{ $opt{motis} } ) { return $self->{backend_id}{motis}{ $opt{motis} }; } @@ -47,6 +50,17 @@ sub get_backend_id { )->hash->{id}; $self->{backend_id}{dbris}{ $opt{dbris} } = $backend_id; } + elsif ( $opt{efa} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + efa => 1, + name => $opt{efa} + } + )->hash->{id}; + $self->{backend_id}{efa}{ $opt{efa} } = $backend_id; + } elsif ( $opt{hafas} ) { $backend_id = $db->select( 'backends', @@ -100,7 +114,7 @@ sub get_backends { $opt{db} //= $self->{pg}->db; my $res = $opt{db}->select( 'backends', - [ 'id', 'name', 'iris', 'hafas', 'dbris', 'motis' ] ); + [ 'id', 'name', 'dbris', 'efa', 'hafas', 'iris', 'motis' ] ); my @ret; while ( my $row = $res->hash ) { @@ -109,9 +123,10 @@ sub get_backends { { id => $row->{id}, name => $row->{name}, - iris => $row->{iris}, dbris => $row->{dbris}, + efa => $row->{efa}, hafas => $row->{hafas}, + iris => $row->{iris}, motis => $row->{motis}, } ); @@ -166,6 +181,44 @@ sub add_or_update { return; } + if ( $opt{efa} ) { + if ( + my $s = $self->get_by_eva( + $stop->id_num, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( + 'stations', + { + name => $stop->full_name, + lat => $stop->latlon->[0], + lon => $stop->latlon->[1], + archived => 0 + }, + { + eva => $stop->id_num, + source => $opt{backend_id} + } + ); + return; + } + $opt{db}->insert( + 'stations', + { + eva => $stop->id_num, + name => $stop->full_name, + lat => $stop->latlon->[0], + lon => $stop->latlon->[1], + source => $opt{backend_id}, + archived => 0 + } + ); + return; + } + if ( $opt{motis} ) { if ( my $s = $self->get_by_external_id( diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 1c3692e..a552633 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -418,7 +418,7 @@ sub get { . 'extract(epoch from registered_at) as registered_at_ts, ' . 'extract(epoch from last_seen) as last_seen_ts, ' . 'extract(epoch from deletion_requested) as deletion_requested_ts, ' - . 'backend_id, backend_name, hafas, dbris, motis', + . 'backend_id, backend_name, dbris, efa, hafas, motis', { id => $uid } )->hash; if ($user) { @@ -458,6 +458,7 @@ sub get { backend_id => $user->{backend_id}, backend_name => $user->{backend_name}, backend_dbris => $user->{dbris}, + backend_efa => $user->{efa}, backend_hafas => $user->{hafas}, backend_motis => $user->{motis}, }; diff --git a/public/static/js/travelynx-actions.js b/public/static/js/travelynx-actions.js index d2316af..370aa33 100644 --- a/public/static/js/travelynx-actions.js +++ b/public/static/js/travelynx-actions.js @@ -79,12 +79,12 @@ function odelay(sched, rt) { return ''; } if (sched < rt) { - return ' (+' + ((rt - sched) / 60) + ')'; + return ' (+' + Math.round((rt - sched) / 60) + ')'; } else if (sched == rt) { return ''; } - return ' (' + ((rt - sched) / 60) + ')'; + return ' (' + Math.round((rt - sched) / 60) + ')'; } function tvly_run(link, req, err_callback) { @@ -195,6 +195,7 @@ function tvly_reg_handlers() { var req = { action: 'checkin', dbris: link.data('dbris'), + efa: link.data('efa'), hafas: link.data('hafas'), motis: link.data('motis'), station: link.data('station'), @@ -210,6 +211,7 @@ function tvly_reg_handlers() { var req = { action: 'checkout', dbris: link.data('dbris'), + efa: link.data('efa'), hafas: link.data('hafas'), motis: link.data('motis'), station: link.data('station'), @@ -243,6 +245,7 @@ function tvly_reg_handlers() { var req = { action: 'cancelled_from', dbris: link.data('dbris'), + efa: link.data('efa'), hafas: link.data('hafas'), motis: link.data('motis'), station: link.data('station'), @@ -256,6 +259,7 @@ function tvly_reg_handlers() { var req = { action: 'cancelled_to', dbris: link.data('dbris'), + efa: link.data('efa'), hafas: link.data('hafas'), motis: link.data('motis'), station: link.data('station'), diff --git a/templates/_departures_efa.html.ep b/templates/_departures_efa.html.ep new file mode 100644 index 0000000..6aec1c8 --- /dev/null +++ b/templates/_departures_efa.html.ep @@ -0,0 +1,54 @@ +<ul class="collection departures"> +% my $orientation_bar_shown = param('train'); +% my $now_epoch = now->epoch; +% for my $result (@{$results}) { + % my $row_class = ''; + % my $link_class = 'action-checkin'; + % if ($result->is_cancelled) { + % $row_class = "cancelled"; + % $link_class = 'action-cancelled-from'; + % } + % if (not $orientation_bar_shown and $result->datetime->epoch < $now_epoch) { + % $orientation_bar_shown = 1; + <li class="collection-item" id="now"> + <strong class="dep-time"> + %= now->strftime('%H:%M') + </strong> + <strong>— Anfragezeitpunkt —</strong> + </li> + % } + <li class="collection-item <%= $link_class %> <%= $row_class %>" + data-efa="<%= $efa %>" + data-station="<%= $result->stop_id_num %>" + data-train="<%= $result->id %>" + data-ts="<%= ($result->sched_datetime // $result->datetime)->epoch %>" + > + <a class="dep-time" href="#"> + %= $result->datetime->strftime('%H:%M') + % if ($result->delay) { + (<%= sprintf('%+d', $result->delay) %>) + % } + % elsif (not defined $result->delay and not $result->is_cancelled) { + <i class="material-icons" aria-label="Keine Echtzeitdaten vorhanden" style="font-size: 16px;">gps_off</i> + % } + </a> + <span class="dep-line <%= $result->type // q{} %>"> + %= $result->line + </span> + <span class="dep-dest"> + % if ($result->is_cancelled) { + Fahrt nach <%= $result->destination %> entfällt + % } + % else { + %= $result->destination + % for my $checkin (@{$checkin_by_train->{$result->id} // []}) { + <span class="followee-checkin"> + <i class="material-icons tiny" aria-label="Eine Person, der du folgst, ist hier eingecheckt">people</i> + <%= $checkin->{followee_name} %> → <%= $checkin->{arr_name} // '???' %> + </span> + % } + % } + </span> + </li> +% } +</ul> diff --git a/templates/changelog.html.ep b/templates/changelog.html.ep index ced431a..2fb89df 100644 --- a/templates/changelog.html.ep +++ b/templates/changelog.html.ep @@ -2,6 +2,21 @@ <div class="row"> <div class="col s12 m1 l1"> + 2.14 + </div> + <div class="col s12 m11 l11"> + <p> + <i class="material-icons left" aria-label="Neues Feature">add</i> + Experimentelle Unterstützung für Checkins via EFA-Backends. + Teilweise ist ein Checkin nur bei Fahrten mit Echtzeitdaten + möglich. Hierbei handelt es sich nach aktuellem Stand um eine + Einschränkung der verwendeten Backends. + </p> + </div> +</div> + +<div class="row"> + <div class="col s12 m1 l1"> 2.13 </div> <div class="col s12 m11 l11"> diff --git a/templates/departures.html.ep b/templates/departures.html.ep index 1745a47..917973b 100644 --- a/templates/departures.html.ep +++ b/templates/departures.html.ep @@ -160,6 +160,9 @@ % if ($dbris) { %= include '_departures_dbris', results => $results, dbris => $dbris; % } + % elsif ($efa) { + %= include '_departures_efa', results => $results, efa => $efa; + % } % elsif ($hafas) { %= include '_departures_hafas', results => $results, hafas => $hafas; % } diff --git a/templates/select_backend.html.ep b/templates/select_backend.html.ep index e54bcfd..e3db44d 100644 --- a/templates/select_backend.html.ep +++ b/templates/select_backend.html.ep @@ -66,19 +66,20 @@ %= end <div class="row"> <div class="col s12"> - <h2 id="help">Hilfe</h2> + <h2 id="help">Details</h2> <p> <strong>Deutsche Bahn: bahn.de</strong> ist eine gute Wahl für Fahrten des Nah-, Regional- und Fernverkehrs innerhalb Deutschlands. - Die Implementierung ist noch recht frisch, bietet jedoch prinzipiell akkurate Echtzeit- und Kartendaten sowie Wagenreihungen. + Dieses Backend bietet überwiegend korrekte Echtzeit- und Kartendaten sowie Wagenreihungen. + Bei Nahverkehrsfahrten sind die Echtzeit- und Kartendaten meist nicht so gut wie bei den APIs des jeweiligen Verkehrsverbunds. <p> - <strong>Deutsche Bahn: IRIS-TTS</strong> liefert Echtzeitdaten (nur am Start- und Zielbahnhof), Wagenreihungen und Verspätungsmeldungen für Regional- und Fernverkehr in Deutschland. Kartendaten sind nur teilweise verfügbar. <strong>ÖBB</strong> liefern Kartendaten und Wagenreihungen für Fernverkehr in Deutschland und Umgebung, jedoch keine Meldungen. Echtzeitdaten sind teilweise verfügbar. </p> <p> - Die restlichen Backends lohnen sich für Fahrten in den zugehörigen Verkehrsverbünden bzw. Ländern. - Im Gegensatz zu bahn.de liefern sie in vielen (aber nicht allen) Fällen auch detaillierte Kartendaten für die dem Verbund zugehörigen Verkehrsmittel. - In Einzelfällen (z.B. BVG) sind sogar Auslastungsdaten eingepflegt. - Bei Fahrten außerhalb von Deutschland und der Schweiz ist <strong>ÖBB</strong> zumeist die beste Wahl. + <strong>Deutsche Bahn: IRIS-TTS</strong> liefert Echtzeitdaten (nur am Start- und Zielbahnhof), Wagenreihungen und Verspätungsmeldungen für Regional- und Fernverkehr in Deutschland. Kartendaten und Angaben zu Unterwegshalten sind nur teilweise verfügbar. Dieses Backend wird nicht mehr weiterentwickelt. Die zugehörige API wird voraussichtlich im Laufe des Jahres 2025 abgeschaltet. + </p> + <p> + <strong>Transitous</strong> ist ein Aggregator für eine Vielzahl von Verkehrsunternehmen. + Die Datenqualität variiert. </p> </div> </div> |