From ef50cbf378ad9c0375332c727b51b2660a62053d Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Wed, 8 Jan 2025 19:14:31 +0100 Subject: Remove DB HAFAS for now Looks like it's gone for good --- lib/Travelynx/Controller/Account.pm | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index faaad0a..8a21ffa 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1070,7 +1070,16 @@ sub backend_form { $backend->{homepage} = 'https://www.bahn.de'; } elsif ( $backend->{hafas} ) { - if ( my $s = $self->hafas->get_service( $backend->{name} ) ) { + + # These backends lack a journey endpoint or are no longer + # operational and are thus useless for travelynx + if ( $backend->{name} eq 'Resrobot' + or $backend->{name} eq 'TPG' + or $backend->{name} eq 'DB' ) + { + $type = undef; + } + elsif ( my $s = $self->hafas->get_service( $backend->{name} ) ) { $type = 'HAFAS'; $backend->{longname} = $s->{name}; $backend->{homepage} = $s->{homepage}; -- cgit v1.2.3 From 838a186ab26d945d74f7f2bdc07edee2b62ee209 Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Sun, 9 Feb 2025 18:51:22 +0100 Subject: Add PKP (and, thus, SOCKS proxy) support --- examples/travelynx.conf | 11 ++++++ lib/Travelynx.pm | 1 + lib/Travelynx/Controller/Account.pm | 11 ++++++ lib/Travelynx/Controller/Traveling.pm | 10 +++++- lib/Travelynx/Helper/HAFAS.pm | 66 +++++++++++++++++++++++++++++------ 5 files changed, 88 insertions(+), 11 deletions(-) (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/examples/travelynx.conf b/examples/travelynx.conf index f8eaac0..c7c4f89 100644 --- a/examples/travelynx.conf +++ b/examples/travelynx.conf @@ -35,6 +35,17 @@ password => die("Changeme!"), }, + # Settings specific to HAFAS backends. + # For instance, the PKP backend is hidden behind a GeoIP filter, hence + # travelynx only supports it if travelynx.conf either indicates that it + # is reachable or specifies a proxy. + hafas => { + PKP => { + # geoip_ok => 1, # <- EITHER THIS + # proxy => 'socks://...', # <- OR THIS + }, + }, + # These settings control the amount and (re)spawn behaviour of travelynx # worker processes as well as IP, port, and PID file. They are suitable for # up to a few dozen concurrent users. If your site has more traffic, you diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index c73f96d..32d1e2f 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -221,6 +221,7 @@ sub startup { my ($self) = @_; state $hafas = Travelynx::Helper::HAFAS->new( log => $self->app->log, + service_config => $self->app->config->{hafas}, main_cache => $self->app->cache_iris_main, realtime_cache => $self->app->cache_iris_rt, root_url => $self->base_url_for('/')->to_abs, diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 8a21ffa..1c54aec 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1079,6 +1079,17 @@ sub backend_form { { $type = undef; } + + # PKP is behind a GeoIP filter. Only list it if travelynx.conf + # indicates that our IP is allowed or provides a proxy. + elsif ( + $backend->{name} eq 'PKP' + and not( $self->app->config->{hafas}{PKP}{geoip_ok} + or $self->app->config->{hafas}{PKP}{proxy} ) + ) + { + $type = undef; + } elsif ( my $s = $self->hafas->get_service( $backend->{name} ) ) { $type = 'HAFAS'; $backend->{longname} = $s->{name}; diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index a0ae43b..dd16c45 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -10,6 +10,7 @@ use DateTime::Format::Strptime; use List::Util qw(uniq min max); use List::UtilsBy qw(max_by uniq_by); use List::MoreUtils qw(first_index); +use Mojo::UserAgent; use Mojo::Promise; use Text::CSV; use Travel::Status::DE::IRIS::Stations; @@ -529,9 +530,16 @@ sub geolocation { if ($hafas_service) { $self->render_later; + my $agent = $self->ua; + if ( my $proxy = $self->app->config->{hafas}{$hafas_service}{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + Travel::Status::DE::HAFAS->new_p( promise => 'Mojo::Promise', - user_agent => $self->ua, + user_agent => $agent, service => $hafas_service, geoSearch => { lat => $lat, diff --git a/lib/Travelynx/Helper/HAFAS.pm b/lib/Travelynx/Helper/HAFAS.pm index 4031663..5b5d343 100644 --- a/lib/Travelynx/Helper/HAFAS.pm +++ b/lib/Travelynx/Helper/HAFAS.pm @@ -12,6 +12,7 @@ use DateTime; use Encode qw(decode); use JSON; use Mojo::Promise; +use Mojo::UserAgent; use Travel::Status::DE::HAFAS; sub _epoch { @@ -42,32 +43,50 @@ sub get_service { sub get_departures_p { my ( $self, %opt ) = @_; + $opt{service} //= 'VRN'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + my $when = ( $opt{timestamp} ? $opt{timestamp}->clone : DateTime->now( time_zone => 'Europe/Berlin' ) )->subtract( minutes => $opt{lookbehind} ); return Travel::Status::DE::HAFAS->new_p( - service => $opt{service} // 'VRN', + service => $opt{service}, station => $opt{eva}, datetime => $when, lookahead => $opt{lookahead} + $opt{lookbehind}, results => 300, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(5), + user_agent => $agent->request_timeout(5), ); } sub search_location_p { my ( $self, %opt ) = @_; + $opt{service} //= 'VRN'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + return Travel::Status::DE::HAFAS->new_p( - service => $opt{service} // 'VRN', + service => $opt{service}, locationSearch => $opt{query}, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(5), + user_agent => $agent->request_timeout(5), ); } @@ -80,13 +99,22 @@ sub get_tripid_p { my $train_desc = $train->type . ' ' . $train->train_no; $train_desc =~ s{^- }{}; + $opt{service} //= 'VRN'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + Travel::Status::DE::HAFAS->new_p( - service => $opt{service} // 'VRN', + service => $opt{service}, journeyMatch => $train_desc, datetime => $train->start, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(10), + user_agent => $agent->request_timeout(10), )->then( sub { my ($hafas) = @_; @@ -132,15 +160,24 @@ sub get_journey_p { my $promise = Mojo::Promise->new; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + $opt{service} //= 'VRN'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + Travel::Status::DE::HAFAS->new_p( - service => $opt{service} // 'VRN', + service => $opt{service}, journey => { id => $opt{trip_id}, }, with_polyline => $opt{with_polyline}, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(10), + user_agent => $agent->request_timeout(10), )->then( sub { my ($hafas) = @_; @@ -173,8 +210,17 @@ sub get_route_p { my $promise = Mojo::Promise->new; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + $opt{service} //= 'VRN'; + + my $agent = $self->{user_agent}; + if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + Travel::Status::DE::HAFAS->new_p( - service => $opt{service} // 'VRN', + service => $opt{service}, journey => { id => $opt{trip_id}, @@ -183,7 +229,7 @@ sub get_route_p { with_polyline => $opt{with_polyline}, cache => $self->{realtime_cache}, promise => 'Mojo::Promise', - user_agent => $self->{user_agent}->request_timeout(10), + user_agent => $agent->request_timeout(10), )->then( sub { my ($hafas) = @_; -- cgit v1.2.3 From dd6d6eb53abe95d8a88ebc59535994866da396e7 Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Tue, 4 Mar 2025 19:41:04 +0100 Subject: Remove VRN; use ÖBB for IRIS augmentation (maps) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Luckily, ÖBB and IRIS agree on virtually all EVA IDs. In some cases, ÖBB uses different station names (e.g. 'Fürth in Bayern' rather than 'Fürth(Bay)'). Closes #206 Closes #207 Closes #208 --- lib/Travelynx/Controller/Account.pm | 1 + lib/Travelynx/Helper/HAFAS.pm | 11 ++++++----- templates/select_backend.html.ep | 8 ++++---- 3 files changed, 11 insertions(+), 9 deletions(-) (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 1c54aec..9cd0edb 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1075,6 +1075,7 @@ sub backend_form { # operational and are thus useless for travelynx if ( $backend->{name} eq 'Resrobot' or $backend->{name} eq 'TPG' + or $backend->{name} eq 'VRN' or $backend->{name} eq 'DB' ) { $type = undef; diff --git a/lib/Travelynx/Helper/HAFAS.pm b/lib/Travelynx/Helper/HAFAS.pm index 5b5d343..ebf44d2 100644 --- a/lib/Travelynx/Helper/HAFAS.pm +++ b/lib/Travelynx/Helper/HAFAS.pm @@ -7,6 +7,7 @@ package Travelynx::Helper::HAFAS; use strict; use warnings; use 5.020; +use utf8; use DateTime; use Encode qw(decode); @@ -43,7 +44,7 @@ sub get_service { sub get_departures_p { my ( $self, %opt ) = @_; - $opt{service} //= 'VRN'; + $opt{service} //= 'ÖBB'; my $agent = $self->{user_agent}; if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { @@ -72,7 +73,7 @@ sub get_departures_p { sub search_location_p { my ( $self, %opt ) = @_; - $opt{service} //= 'VRN'; + $opt{service} //= 'ÖBB'; my $agent = $self->{user_agent}; if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { @@ -99,7 +100,7 @@ sub get_tripid_p { my $train_desc = $train->type . ' ' . $train->train_no; $train_desc =~ s{^- }{}; - $opt{service} //= 'VRN'; + $opt{service} //= 'ÖBB'; my $agent = $self->{user_agent}; if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { @@ -160,7 +161,7 @@ sub get_journey_p { my $promise = Mojo::Promise->new; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); - $opt{service} //= 'VRN'; + $opt{service} //= 'ÖBB'; my $agent = $self->{user_agent}; if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { @@ -210,7 +211,7 @@ sub get_route_p { my $promise = Mojo::Promise->new; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); - $opt{service} //= 'VRN'; + $opt{service} //= 'ÖBB'; my $agent = $self->{user_agent}; if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) { diff --git a/templates/select_backend.html.ep b/templates/select_backend.html.ep index 8af157d..f55c75c 100644 --- a/templates/select_backend.html.ep +++ b/templates/select_backend.html.ep @@ -34,15 +34,15 @@

Hilfe

- Leider gibt es seit der Abschaltung des DB HAFAS am 8. Januar 2025 derzeit kein Backend, welches allgemein für Nah- und Fernverkehr in Deutschland nutzbar ist. - Der VRN kommt voraussichtlich am ehesten an Qualität und Umfang der im DB HAFAS verfügbaren Daten heran. + Leider gibt es seit der Abschaltung des DB HAFAS am 8. Januar 2025 sowie des VRN HAFAS am 3. März 2025 derzeit kein Backend, welches allgemein für Nah- und Fernverkehr in Deutschland nutzbar ist. + Das Deutsche Bahn (IRIS-TTS) eignet sich für Fahrten mit S-Bahnen, Regional- und Fernzügen. Im Übrigen muss je nach Verkehrsmittel, Region und Wünschen an die verfügbaren Daten hier ein geeignetes Backend ausgewählt werden. + In einzelnen Regionen steht kein geeignetes Backend für Nahverkehrsfahrten zur Verfügung. Abhilfe ist in Arbeit.

- VRN ist eine gute Wahl für Nah-, Regional- und Fernverkehr in Deutschland. Hier gibt es meist Echtzeitdaten, allgemeine Meldungen, Wagenreihungen und Kartendaten. Deutsche Bahn (IRIS-TTS) liefert Echtzeitdaten, Wagenreihungen und Verspätungsmeldungen für Regional- und Fernverkehr in Deutschland. In vielen Fällen sind auch allgemeine Meldungen und Kartendaten verfügbar. - ÖBB liefern Kartendaten und Wagenreihungen für Fernverkehr in Deutschland, jedoch keine Meldungen. Echtzeitdaten sind teilweise verfügbar. + ÖBB liefern Kartendaten und Wagenreihungen für Fernverkehr in Deutschland und Umgebung, jedoch keine Meldungen. Echtzeitdaten sind teilweise verfügbar.

Die restlichen Backends lohnen sich für Fahrten in den zugehörigen Verkehrsverbünden bzw. Ländern. -- cgit v1.2.3 From a9b5a18943c3e2070703e745cd1131a02fd20365 Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Sun, 23 Mar 2025 18:07:50 +0100 Subject: Preliminary DBRIS support (not user-accessible yet) working: * checkin * checkout * realtime data * polylines * carriage formation (long-distance only) to do: * geolocation * redirects after checkout / undo * traewelling sync * use dbris by default --- lib/Travelynx.pm | 170 +++++++++++++++++++++++++++++++++- lib/Travelynx/Command/database.pm | 153 ++++++++++++++++++++++++++++++ lib/Travelynx/Command/work.pm | 105 +++++++++++++++++++++ lib/Travelynx/Controller/Account.pm | 19 ++-- lib/Travelynx/Controller/Traveling.pm | 103 +++++++++++++++++--- lib/Travelynx/Helper/DBRIS.pm | 138 +++++++++++++++++++++++++++ lib/Travelynx/Model/InTransit.pm | 132 +++++++++++++++++++++++++- lib/Travelynx/Model/Journeys.pm | 11 ++- lib/Travelynx/Model/Stations.pm | 82 ++++++++++++---- lib/Travelynx/Model/Users.pm | 9 +- public/static/js/travelynx-actions.js | 4 + templates/_departures_dbris.html.ep | 54 +++++++++++ templates/departures.html.ep | 18 ++-- templates/landingpage.html.ep | 1 + 14 files changed, 945 insertions(+), 54 deletions(-) create mode 100644 lib/Travelynx/Helper/DBRIS.pm create mode 100644 templates/_departures_dbris.html.ep (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index 2b7fdf5..98ba4aa 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -21,6 +21,7 @@ use List::UtilsBy qw(uniq_by); use List::MoreUtils qw(first_index); use Travel::Status::DE::DBRIS::Formation; use Travelynx::Helper::DBDB; +use Travelynx::Helper::DBRIS; use Travelynx::Helper::HAFAS; use Travelynx::Helper::IRIS; use Travelynx::Helper::Sendmail; @@ -216,6 +217,19 @@ sub startup { } ); + $self->helper( + dbris => sub { + my ($self) = @_; + state $dbris = Travelynx::Helper::DBRIS->new( + log => $self->app->log, + 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( hafas => sub { my ($self) = @_; @@ -452,6 +466,9 @@ sub startup { return Mojo::Promise->reject('You are already checked in'); } + if ( $opt{dbris} ) { + return $self->_checkin_dbris_p(%opt); + } if ( $opt{hafas} ) { return $self->_checkin_hafas_p(%opt); } @@ -530,6 +547,146 @@ sub startup { } ); + $self->helper( + '_checkin_dbris_p' => sub { + my ( $self, %opt ) = @_; + + my $station = $opt{station}; + my $train_id = $opt{train_id}; + 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; + + $self->dbris->get_journey_p( + trip_id => $train_id, + with_polyline => 1 + )->then( + sub { + my ($journey) = @_; + my $found; + for my $stop ( $journey->route ) { + if ( $stop->eva eq $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 '$train_id'" + ); + return; + } + for my $stop ( $journey->route ) { + $self->stations->add_or_update( + stop => $stop, + db => $db, + dbris => 'bahn.de', + ); + } + eval { + $self->in_transit->add( + uid => $uid, + db => $db, + journey => $journey, + stop => $found, + data => { trip_id => $train_id }, + backend_id => $self->stations->get_backend_id( + dbris => 'bahn.de' + ), + ); + }; + 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}->eva + ] + ); + push( @station_list, $coord->{stop}->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->train + . ' as it only consists of straight lines between stops.' + ); + } + else { + $polyline = { + from_eva => ( $journey->route )[0]->eva, + to_eva => ( $journey->route )[-1]->eva, + 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' ); + $self->add_wagonorder( + uid => $uid, + train_id => $train_id, + is_departure => 1, + eva => $found->eva, + datetime => $found->sched_dep, + train_type => $journey->type, + train_no => $journey->number + ); + $self->add_stationinfo( $uid, 1, $train_id, + $found->eva ); + } + + $promise->resolve($journey); + } + )->catch( + sub { + my ($err) = @_; + $promise->reject($err); + return; + } + )->wait; + + return $promise; + } + ); + $self->helper( '_checkin_hafas_p' => sub { my ( $self, %opt ) = @_; @@ -799,8 +956,8 @@ sub startup { return $promise->resolve( 0, 'race condition' ); } - if ( $user->{is_hafas} ) { - return $self->_checkout_hafas_p(%opt); + if ( $user->{is_dbris} or $user->{is_hafas} ) { + return $self->_checkout_journey_p(%opt); } my $now = DateTime->now( time_zone => 'Europe/Berlin' ); @@ -1049,7 +1206,7 @@ sub startup { ); $self->helper( - '_checkout_hafas_p' => sub { + '_checkout_journey_p' => sub { my ( $self, %opt ) = @_; my $station = $opt{station}; @@ -1840,6 +1997,7 @@ sub startup { cancellation => $latest_cancellation, backend_id => $latest->{backend_id}, backend_name => $latest->{backend_name}, + is_dbris => $latest->{is_dbris}, is_iris => $latest->{is_iris}, is_hafas => $latest->{is_hafas}, journey_id => $latest->{journey_id}, @@ -1901,8 +2059,10 @@ sub startup { ) ? \1 : \0, comment => $status->{comment}, backend => { - id => $status->{backend_id}, - type => $status->{is_hafas} ? 'HAFAS' : 'IRIS-TTS', + id => $status->{backend_id}, + type => $status->{ds_dbris} ? 'DBRIS' + : $status->{is_hafas} ? 'HAFAS' + : 'IRIS-TTS', name => $status->{backend_name}, }, fromStation => { diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index 6ba80a3..f72a38c 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -2701,6 +2701,159 @@ qq{select distinct checkout_station_id from in_transit where backend_id = 0;} } ); }, + + # v59 -> v60 + # Add bahn.de / DBRIS backend + sub { + my ($db) = @_; + $db->insert( + 'backends', + { + iris => 0, + hafas => 0, + efa => 0, + ris => 1, + name => 'bahn.de', + }, + ); + $db->query( + qq{ + update schema_version set version = 60; + } + ); + }, + + # v60 -> v61 + # Rename "ris" / "is_ris" to "dbris" / "is_dbris", as it is DB-specific + sub { + my ($db) = @_; + $db->query( + qq{ + drop view in_transit_str; + drop view journeys_str; + drop view users_with_backend; + drop view follows_in_transit; + alter table backends rename column ris to dbris; + create view in_transit_str as select + user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.name as backend_name, in_transit.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join backends as backend on in_transit.backend_id = backend.id + ; + create view journeys_str as select + journeys.id as journey_id, user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.name as backend_name, journeys.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, edited, route, messages, user_data, + dep_platform, arr_platform + from journeys + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source + left join backends as backend on journeys.backend_id = backend.id + ; + create view users_with_backend as select + users.id as id, users.name as name, status, public_level, + email, password, registered_at, last_seen, + deletion_requested, deletion_notified, use_history, + accept_follows, notifications, profile, backend_id, iris, + hafas, efa, dbris, backend.name as backend_name + from users + left join backends as backend on users.backend_id = backend.id + ; + create view follows_in_transit as select + r1.subject_id as follower_id, user_id as followee_id, + users.name as followee_name, + train_type, train_line, train_no, train_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.name as backend_name, in_transit.backend_id as backend_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join backends as backend on in_transit.backend_id = backend.id + order by checkin_time desc + ; + update schema_version set version = 61; + } + ); + }, ); sub sync_stations { diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm index 5385e9b..55c2005 100644 --- a/lib/Travelynx/Command/work.pm +++ b/lib/Travelynx/Command/work.pm @@ -49,6 +49,111 @@ sub run { my $arr = $entry->{arr_eva}; my $train_id = $entry->{train_id}; + if ( $entry->{is_dbris} ) { + + eval { + + $self->app->dbris->get_journey_p( trip_id => $train_id )->then( + sub { + my ($journey) = @_; + + my $found_dep; + my $found_arr; + for my $stop ( $journey->route ) { + if ( $stop->eva == $dep ) { + $found_dep = $stop; + } + if ( $arr and $stop->eva == $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_dbris( + uid => $uid, + journey => $journey, + stop => $found_dep, + dep_eva => $dep, + arr_eva => $arr, + train_id => $train_id, + ); + } + if ( $found_dep->sched_dep + and $found_dep->dep->epoch > $now->epoch ) + { + $self->app->add_wagonorder( + uid => $uid, + train_id => $train_id, + is_departure => 1, + eva => $dep, + datetime => $found_dep->sched_dep, + train_type => $journey->type, + train_no => $journey->number, + ); + $self->app->add_stationinfo( $uid, 1, + $train_id, $found_dep->eva ); + } + + if ( $found_arr and $found_arr->rt_arr ) { + $self->app->in_transit->update_arrival_dbris( + uid => $uid, + journey => $journey, + stop => $found_arr, + dep_eva => $dep, + arr_eva => $arr + ); + if ( $found_arr->arr->epoch - $now->epoch < 600 ) { + $self->app->add_wagonorder( + uid => $uid, + train_id => $train_id, + is_arrival => 1, + eva => $arr, + datetime => $found_arr->sched_dep, + train_type => $journey->type, + train_no => $journey->number, + ); + $self->app->add_stationinfo( $uid, 0, + $train_id, $found_dep->eva, + $found_arr->eva ); + } + } + } + )->catch( + sub { + my ($err) = @_; + $self->app->log->error( +"work($uid) @ DBRIS $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) @ DBRIS $entry->{backend_name}: $@"); + } + next; + } + if ( $entry->{is_hafas} ) { eval { diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 9cd0edb..43b0683 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1066,9 +1066,17 @@ sub backend_form { if ( $backend->{iris} ) { $type = 'IRIS-TTS'; $backend->{name} = 'IRIS'; - $backend->{longname} = 'Deutsche Bahn (IRIS-TTS)'; + $backend->{longname} = 'Deutsche Bahn: IRIS-TTS'; $backend->{homepage} = 'https://www.bahn.de'; } + elsif ( $backend->{dbris} ) { + $type = 'DBRIS'; + $backend->{longname} = 'Deutsche Bahn: bahn.de'; + $backend->{homepage} = 'https://www.bahn.de'; + + # not ready for production yet + $type = undef; + } elsif ( $backend->{hafas} ) { # These backends lack a journey endpoint or are no longer @@ -1135,14 +1143,11 @@ sub backend_form { $backend->{type} = $type; } - # These backends lack a journey endpoint and are useless for travelynx - @backends - = grep { $_->{name} ne 'Resrobot' and $_->{name} ne 'TPG' } @backends; - my $iris = shift @backends; - @backends - = sort { $a->{name} cmp $b->{name} } grep { $_->{type} } @backends; + @backends = map { $_->[1] } + sort { $a->[0] cmp $b->[0] } + map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends; unshift( @backends, $iris ); diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 1ca9d4a..a6e56b9 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -42,6 +42,13 @@ sub get_connecting_trains_p { my $promise = Mojo::Promise->new; + if ( $user->{backend_dbris} ) { + + # We do get a little bit of via information, so this might work in some + # cases. But not reliably. Probably best to leave it out entirely then. + return $promise->reject; + } + if ( $opt{eva} ) { if ( $use_history & 0x01 ) { $eva = $opt{eva}; @@ -106,6 +113,8 @@ sub get_connecting_trains_p { my $iris_promise = Mojo::Promise->new; my %via_count = map { $_->{name} => 0 } @destinations; + my $backend + = $self->stations->get_backend( backend_id => $opt{backend_id} ); if ( $opt{backend_id} == 0 ) { $self->iris->get_departures_p( station => $eva, @@ -260,9 +269,11 @@ sub get_connecting_trains_p { } )->wait; } - else { - my $hafas_service - = $self->stations->get_hafas_name( backend_id => $opt{backend_id} ); + elsif ( $backend->{dbris} ) { + ...; + } + elsif ( $backend->{hafas} ) { + my $hafas_service = $backend->{name}; $self->hafas->get_departures_p( service => $hafas_service, eva => $eva, @@ -524,10 +535,19 @@ sub geolocation { return; } - my $hafas_service - = $self->stations->get_hafas_name( backend_id => $backend_id ); + my ( $dbris_service, $hafas_service ); + my $backend = $self->stations->get_backend( backend_id => $backend_id ); + if ( $backend->{dbris} ) { + $dbris_service = $backend->{name}; + } + elsif ( $backend->{hafas} ) { + $hafas_service = $backend->{name}; + } - if ($hafas_service) { + if ($dbris_service) { + ...; + } + elsif ($hafas_service) { $self->render_later; my $agent = $self->ua; @@ -588,6 +608,7 @@ sub geolocation { lon => $_->[0][3], lat => $_->[0][4], distance => $_->[1], + dbris => 0, hafas => 0, } } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon, @@ -656,6 +677,7 @@ sub travel_action { $promise->then( sub { return $self->checkin_p( + dbris => $params->{dbris}, hafas => $params->{hafas}, station => $params->{station}, train_id => $params->{train}, @@ -687,7 +709,10 @@ sub travel_action { my ( $still_checked_in, undef ) = @_; if ( my $destination = $params->{dest} ) { my $station_link = '/s/' . $destination; - if ( $status->{is_hafas} ) { + if ( $status->{is_dbris} ) { + $station_link .= '?dbris=' . $status->{backend_name}; + } + elsif ( $status->{is_hafas} ) { $station_link .= '?hafas=' . $status->{backend_name}; } $self->render( @@ -723,7 +748,10 @@ sub travel_action { sub { my ( $still_checked_in, $error ) = @_; my $station_link = '/s/' . $params->{station}; - if ( $status->{is_hafas} ) { + if ( $status->{is_dbris} ) { + $station_link .= '?dbris=' . $status->{backend_name}; + } + elsif ( $status->{is_hafas} ) { $station_link .= '?hafas=' . $status->{backend_name}; } @@ -774,7 +802,14 @@ sub travel_action { else { my $redir = '/'; if ( $status->{checked_in} or $status->{cancelled} ) { - if ( $status->{is_hafas} ) { + if ( $status->{is_dbris} ) { + $redir + = '/s/' + . $status->{dep_eva} + . '?dbris=' + . $status->{backend_name}; + } + elsif ( $status->{is_hafas} ) { $redir = '/s/' . $status->{dep_eva} @@ -796,6 +831,7 @@ sub travel_action { elsif ( $params->{action} eq 'cancelled_from' ) { $self->render_later; $self->checkin_p( + dbris => $params->{dbris}, hafas => $params->{hafas}, station => $params->{station}, train_id => $params->{train}, @@ -925,10 +961,19 @@ sub station { $timestamp = DateTime->now( time_zone => 'Europe/Berlin' ); } + my $dbris_service = $self->param('dbris') + // ( $user->{backend_dbris} ? $user->{backend_name} : undef ); my $hafas_service = $self->param('hafas') // ( $user->{backend_hafas} ? $user->{backend_name} : undef ); my $promise; - if ($hafas_service) { + if ($dbris_service) { + $promise = $self->dbris->get_departures_p( + station => $station, + timestamp => $timestamp, + lookbehind => 30, + ); + } + elsif ($hafas_service) { $promise = $self->hafas->get_departures_p( service => $hafas_service, eva => $station, @@ -954,7 +999,22 @@ sub station { my $now_within_range = abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0; - if ($hafas_service) { + if ($dbris_service) { + + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->dep->epoch ] } $status->results; + + $status = { + station_eva => $station, + related_stations => [], + }; + + if ( $station =~ m{ [@] O = (? [^@]+ ) [@] }x ) { + $status->{station_name} = $+{name}; + } + } + elsif ($hafas_service) { @results = map { $_->[0] } sort { $b->[1] <=> $a->[1] } @@ -1039,6 +1099,7 @@ sub station { $self->render( 'departures', user => $user, + dbris => $dbris_service, hafas => $hafas_service, eva => $status->{station_eva}, datetime => $timestamp, @@ -1058,6 +1119,7 @@ sub station { $self->render( 'departures', user => $user, + dbris => $dbris_service, hafas => $hafas_service, eva => $status->{station_eva}, datetime => $timestamp, @@ -1076,6 +1138,7 @@ sub station { $self->render( 'departures', user => $user, + dbris => $dbris_service, hafas => $hafas_service, eva => $status->{station_eva}, datetime => $timestamp, @@ -1174,7 +1237,23 @@ sub redirect_to_station { my ($self) = @_; my $station = $self->param('station'); - $self->redirect_to("/s/${station}"); + if ( $self->param('backend_dbris') ) { + $self->render_later; + $self->dbris->get_station_id_p($station)->then( + sub { + my ($dbris_station) = @_; + $self->redirect_to( '/s/' . $dbris_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + $self->redirect_to('/'); + } + )->wait; + } + else { + $self->redirect_to("/s/${station}"); + } } sub cancelled { diff --git a/lib/Travelynx/Helper/DBRIS.pm b/lib/Travelynx/Helper/DBRIS.pm new file mode 100644 index 0000000..e647cc5 --- /dev/null +++ b/lib/Travelynx/Helper/DBRIS.pm @@ -0,0 +1,138 @@ +package Travelynx::Helper::DBRIS; + +# Copyright (C) 2025 Birte Kristina Friesel +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +use strict; +use warnings; +use 5.020; +use utf8; + +use DateTime; +use Encode qw(decode); +use JSON; +use Mojo::Promise; +use Mojo::UserAgent; +use Travel::Status::DE::DBRIS; + +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_station_id_p { + my ( $self, $station_name ) = @_; + my $promise = Mojo::Promise->new; + Travel::Status::DE::DBRIS->new_p( + locationSearch => $station_name, + cache => $self->{cache}, + lwp_options => { + timeout => 10, + agent => $self->{header}{'User-Agent'}, + }, + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + )->then( + sub { + my ($dbris) = @_; + my $found; + for my $result ( $dbris->results ) { + if ( defined $result->eva ) { + $promise->resolve($result); + return; + } + } + $promise->reject("Unable to find station '$station_name'"); + return; + } + )->catch( + sub { + my ($err) = @_; + $promise->reject("'$err' while trying to look up '$station_name'"); + return; + } + )->wait; + return $promise; +} + +sub get_departures_p { + my ( $self, %opt ) = @_; + + my $agent = $self->{user_agent}; + + if ( $opt{station} =~ m{ [@] L = (? \d+ ) [@] }x ) { + $opt{station} = { + eva => $+{eva}, + id => $opt{station}, + }; + } + + my $when = ( + $opt{timestamp} + ? $opt{timestamp}->clone + : DateTime->now( time_zone => 'Europe/Berlin' ) + )->subtract( minutes => $opt{lookbehind} ); + return Travel::Status::DE::DBRIS->new_p( + station => $opt{station}, + datetime => $when, + cache => $self->{cache}, + promise => 'Mojo::Promise', + user_agent => $agent->request_timeout(10), + ); +} + +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}; + if ( my $proxy = $self->{service_config}{dbris}{proxy} ) { + $agent = Mojo::UserAgent->new; + $agent->proxy->http($proxy); + $agent->proxy->https($proxy); + } + + Travel::Status::DE::DBRIS->new_p( + journey => $opt{trip_id}, + with_polyline => $opt{with_polyline}, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => $agent->request_timeout(10), + )->then( + sub { + my ($dbris) = @_; + my $journey = $dbris->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/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm index 43ecb90..2b9832c 100644 --- a/lib/Travelynx/Model/InTransit.pm +++ b/lib/Travelynx/Model/InTransit.pm @@ -104,6 +104,8 @@ sub add { my $json = JSON->new; if ($train) { + + # IRIS $db->insert( 'in_transit', { @@ -134,7 +136,9 @@ sub add { } ); } - elsif ( $journey and $stop ) { + elsif ( $journey and $stop and $journey->can('id') ) { + + # HAFAS my @route; my $product = $journey->product_at( $stop->loc->eva ) // $journey->product; @@ -188,6 +192,56 @@ sub add { } ); } + elsif ( $journey and $stop ) { + + # DBRIS + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $j_stop->name, + $j_stop->eva, + { + 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, + load => undef, + lat => $j_stop->lat, + lon => $j_stop->lon, + } + ] + ); + } + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => $stop->{dep_cancelled} + ? 1 + : 0, + checkin_station_id => $stop->eva, + checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ), + dep_platform => $stop->platform, + train_type => $journey->type, + train_no => $journey->number, + train_id => $data->{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, + } + ); + } else { die('neither train nor journey specified'); } @@ -731,6 +785,33 @@ sub update_departure_cancelled { return $rows; } +sub update_departure_dbris { + 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; + + # 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', + { + real_departure => $stop->{rt_dep}, + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_departure_hafas { my ( $self, %opt ) = @_; my $uid = $opt{uid}; @@ -800,6 +881,55 @@ sub update_arrival { return $rows; } +sub update_arrival_dbris { + 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 @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $j_stop->name, + $j_stop->eva, + { + 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, + lat => $j_stop->lat, + lon => $j_stop->lon, + } + ] + ); + } + + # 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', + { + real_arrival => $stop->{rt_arr}, + route => $json->encode( [@route] ), + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_arrival_hafas { my ( $self, %opt ) = @_; my $uid = $opt{uid}; diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index 905c426..f5bc9f1 100755 --- a/lib/Travelynx/Model/Journeys.pm +++ b/lib/Travelynx/Model/Journeys.pm @@ -549,7 +549,7 @@ sub get { my @select = ( - qw(journey_id is_iris is_hafas backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility) + qw(journey_id is_dbris is_iris is_hafas backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility) ); my %where = ( user_id => $uid, @@ -607,6 +607,7 @@ sub get { my $ref = { id => $entry->{journey_id}, + is_dbris => $entry->{is_dbris}, is_iris => $entry->{is_iris}, is_hafas => $entry->{is_hafas}, backend_name => $entry->{backend_name}, @@ -870,8 +871,8 @@ sub get_latest_checkout_stations { my $res = $db->select( 'journeys_str', [ - 'arr_name', 'arr_eva', 'train_id', 'backend_id', - 'backend_name', 'is_hafas' + 'arr_name', 'arr_eva', 'train_id', 'backend_id', + 'backend_name', 'is_dbris', 'is_hafas' ], { user_id => $uid, @@ -895,6 +896,7 @@ sub get_latest_checkout_stations { { name => $row->{arr_name}, eva => $row->{arr_eva}, + dbris => $row->{is_dbris} ? $row->{backend_name} : 0, hafas => $row->{is_hafas} ? $row->{backend_name} : 0, backend_id => $row->{backend_id}, } @@ -1883,7 +1885,8 @@ sub get_connection_targets { ); my @destinations = $res->hashes->grep( sub { shift->{count} >= $min_count } ) - ->map( sub { shift->{dest} } )->each; + ->map( sub { shift->{dest} } ) + ->each; @destinations = $self->{stations}->get_by_evas( backend_id => $opt{backend_id}, evas => [@destinations] diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm index 76fd452..3d6549f 100644 --- a/lib/Travelynx/Model/Stations.pm +++ b/lib/Travelynx/Model/Stations.pm @@ -25,11 +25,25 @@ sub get_backend_id { 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} }; + } my $db = $opt{db} // $self->{pg}->db; my $backend_id = 0; - if ( $opt{hafas} ) { + if ( $opt{dbris} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + dbris => 1, + name => $opt{dbris} + } + )->hash->{id}; + $self->{backend_id}{dbris}{ $opt{dbris} } = $backend_id; + } + elsif ( $opt{hafas} ) { $backend_id = $db->select( 'backends', ['id'], @@ -44,31 +58,25 @@ sub get_backend_id { return $backend_id; } -sub get_hafas_name { +sub get_backend { my ( $self, %opt ) = @_; - if ( exists $self->{hafas_name}{ $opt{backend_id} } ) { - return $self->{hafas_name}{ $opt{backend_id} }; + if ( $self->{backend_cache}{ $opt{backend_id} } ) { + return $self->{backend_cache}{ $opt{backend_id} }; } - my $db = $opt{db} // $self->{pg}->db; - my $hafas_name; + my $db = $opt{db} // $self->{pg}->db; my $ret = $db->select( 'backends', - ['name'], + '*', { - hafas => 1, - id => $opt{backend_id}, + id => $opt{backend_id}, } )->hash; - if ($ret) { - $hafas_name = $ret->{name}; - } - - $self->{hafas_name}{ $opt{backend_id} } = $hafas_name; + $self->{backend_cache}{ $opt{backend_id} } = $ret; - return $hafas_name; + return $ret; } sub get_backends { @@ -76,7 +84,8 @@ sub get_backends { $opt{db} //= $self->{pg}->db; - my $res = $opt{db}->select( 'backends', [ 'id', 'name', 'iris', 'hafas' ] ); + my $res = $opt{db} + ->select( 'backends', [ 'id', 'name', 'iris', 'hafas', 'dbris' ] ); my @ret; while ( my $row = $res->hash ) { @@ -86,6 +95,7 @@ sub get_backends { id => $row->{id}, name => $row->{name}, iris => $row->{iris}, + dbris => $row->{dbris}, hafas => $row->{hafas}, } ); @@ -97,11 +107,49 @@ sub get_backends { sub add_or_update { my ( $self, %opt ) = @_; my $stop = $opt{stop}; - my $loc = $stop->loc; $opt{db} //= $self->{pg}->db; $opt{backend_id} //= $self->get_backend_id(%opt); + if ( $opt{dbris} ) { + if ( + my $s = $self->get_by_eva( + $stop->eva, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( + 'stations', + { + name => $stop->name, + lat => $stop->lat, + lon => $stop->lon, + archived => 0 + }, + { + eva => $stop->eva, + source => $opt{backend_id} + } + ); + return; + } + $opt{db}->insert( + 'stations', + { + eva => $stop->eva, + name => $stop->name, + lat => $stop->lat, + lon => $stop->lon, + source => $opt{backend_id}, + archived => 0 + } + ); + return; + } + + my $loc = $stop->loc; if ( my $s = $self->get_by_eva( $loc->eva, diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 7d3777b..e3d6f7a 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -209,7 +209,11 @@ sub set_backend { my ( $self, %opt ) = @_; $opt{db} //= $self->{pg}->db; - $opt{db}->update('users', {backend_id => $opt{backend_id}}, {id => $opt{uid}}); + $opt{db}->update( + 'users', + { backend_id => $opt{backend_id} }, + { id => $opt{uid} } + ); } sub set_privacy { @@ -414,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', + . 'backend_id, backend_name, hafas, dbris', { id => $uid } )->hash; if ($user) { @@ -453,6 +457,7 @@ sub get { : undef, backend_id => $user->{backend_id}, backend_name => $user->{backend_name}, + backend_dbris => $user->{dbris}, backend_hafas => $user->{hafas}, }; } diff --git a/public/static/js/travelynx-actions.js b/public/static/js/travelynx-actions.js index d4ddf45..0d2de5a 100644 --- a/public/static/js/travelynx-actions.js +++ b/public/static/js/travelynx-actions.js @@ -194,6 +194,7 @@ function tvly_reg_handlers() { var link = $(this); var req = { action: 'checkin', + dbris: link.data('dbris'), hafas: link.data('hafas'), station: link.data('station'), train: link.data('train'), @@ -206,6 +207,7 @@ function tvly_reg_handlers() { var link = $(this); var req = { action: 'checkout', + dbris: link.data('dbris'), hafas: link.data('hafas'), station: link.data('station'), force: link.data('force'), @@ -237,6 +239,7 @@ function tvly_reg_handlers() { var link = $(this); var req = { action: 'cancelled_from', + dbris: link.data('dbris'), hafas: link.data('hafas'), station: link.data('station'), ts: link.data('ts'), @@ -248,6 +251,7 @@ function tvly_reg_handlers() { var link = $(this); var req = { action: 'cancelled_to', + dbris: link.data('dbris'), hafas: link.data('hafas'), station: link.data('station'), force: true, diff --git a/templates/_departures_dbris.html.ep b/templates/_departures_dbris.html.ep new file mode 100644 index 0000000..14426e8 --- /dev/null +++ b/templates/_departures_dbris.html.ep @@ -0,0 +1,54 @@ +

diff --git a/templates/departures.html.ep b/templates/departures.html.ep index a86a7b5..6e2a98a 100644 --- a/templates/departures.html.ep +++ b/templates/departures.html.ep @@ -9,7 +9,10 @@
% my $self_link = url_for('sstation', station => $station // param('station')); - % if (param('hafas')) { + % if (param('dbris')) { + <%= param('dbris') %> + % } + % elsif (param('hafas')) { <%= param('hafas') %> % } % else { @@ -93,8 +96,8 @@
- % if ($hafas) { - früher + % if ($dbris or $hafas) { + früher % }
@@ -103,8 +106,8 @@ % }
- % if ($hafas) { - später + % if ($dbris or $hafas) { + später % }
@@ -139,7 +142,10 @@ % }

% if (not $user_status->{checked_in} or ($can_check_out and $user_status->{arr_eva} and $user_status->{arrival_countdown} <= 0)) { - % if ($hafas) { + % if ($dbris) { + %= include '_departures_dbris', results => $results, dbris => $dbris; + % } + % elsif ($hafas) { %= include '_departures_hafas', results => $results, hafas => $hafas; % } % else { diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep index 8b6eb3f..856fdef 100644 --- a/templates/landingpage.html.ep +++ b/templates/landingpage.html.ep @@ -60,6 +60,7 @@ + %= hidden_field backend_dbris => $user->{backend_dbris}
%= text_field 'station', id => 'station', class => 'autocomplete contrast-color-text', autocomplete => 'off', required => undef -- cgit v1.2.3 From 164253d1a96f676be3d96faa70006a71b72322d6 Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Sun, 23 Mar 2025 18:50:57 +0100 Subject: dbris is almost ready for production Agenda: * geolocation * traewelling sync * check whether cancelled trains / stops are handled appropriately * switch manual journey entry to dbris once sufficient station data has been gathered * switch default to dbris at some time in the future --- lib/Travelynx/Controller/Account.pm | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 43b0683..b8bd2f4 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1070,12 +1070,10 @@ sub backend_form { $backend->{homepage} = 'https://www.bahn.de'; } elsif ( $backend->{dbris} ) { - $type = 'DBRIS'; - $backend->{longname} = 'Deutsche Bahn: bahn.de'; + $type = 'DBRIS'; + $backend->{longname} + = 'Deutsche Bahn: bahn.de (UNFERTIG / BETA-TEST – Fehler und fehlerhafte Daten sind möglich)'; $backend->{homepage} = 'https://www.bahn.de'; - - # not ready for production yet - $type = undef; } elsif ( $backend->{hafas} ) { -- cgit v1.2.3 From dcf5e48bf8285b69336ed9affbcadcecb5a385b5 Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Mon, 24 Mar 2025 17:21:41 +0100 Subject: bahn.de is reasonably ready now. I think. --- lib/Travelynx/Controller/Account.pm | 2 +- templates/select_backend.html.ep | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index b8bd2f4..0a424cc 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1072,7 +1072,7 @@ sub backend_form { elsif ( $backend->{dbris} ) { $type = 'DBRIS'; $backend->{longname} - = 'Deutsche Bahn: bahn.de (UNFERTIG / BETA-TEST – Fehler und fehlerhafte Daten sind möglich)'; + = 'Deutsche Bahn: bahn.de'; $backend->{homepage} = 'https://www.bahn.de'; } elsif ( $backend->{hafas} ) { diff --git a/templates/select_backend.html.ep b/templates/select_backend.html.ep index 5397542..19eaee5 100644 --- a/templates/select_backend.html.ep +++ b/templates/select_backend.html.ep @@ -34,19 +34,18 @@

Hilfe

- Leider gibt es seit der Abschaltung des DB HAFAS am 8. Januar 2025 sowie des VRN HAFAS am 3. März 2025 derzeit kein Backend, welches allgemein für Nah- und Fernverkehr in Deutschland nutzbar ist. - Deutsche Bahn (IRIS-TTS) eignet sich für Fahrten mit S-Bahnen, Regional- und Fernzügen. - Im Übrigen muss je nach Verkehrsmittel, Region und Wünschen an die verfügbaren Daten hier ein geeignetes Backend ausgewählt werden. - In einzelnen Regionen steht kein geeignetes Backend für Nahverkehrsfahrten zur Verfügung. - Abhilfe ist in Arbeit. -

+ Deutsche Bahn: bahn.de 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. + Wagenreihungen sind nur bei Fahrten des Fernverkehrs sowie Zügen ohne Liniennummer verfügbar. + Verspätungsmeldungen werden aktuell nicht berücksichtigt. + bahn.de ist das einzige Backend, welches Synchronisierung mit Träwelling unterstützt.

- Deutsche Bahn (IRIS-TTS) liefert Echtzeitdaten (nur am Start- und Zielbahnhof), Wagenreihungen und Verspätungsmeldungen für Regional- und Fernverkehr in Deutschland. In vielen Fällen sind auch Kartendaten verfügbar. + Deutsche Bahn: IRIS-TTS liefert Echtzeitdaten (nur am Start- und Zielbahnhof), Wagenreihungen und Verspätungsmeldungen für Regional- und Fernverkehr in Deutschland. In vielen Fällen sind auch Kartendaten verfügbar. ÖBB liefern Kartendaten und Wagenreihungen für Fernverkehr in Deutschland und Umgebung, jedoch keine Meldungen. Echtzeitdaten sind teilweise verfügbar.

Die restlichen Backends lohnen sich für Fahrten in den zugehörigen Verkehrsverbünden bzw. Ländern. - Im Gegensatz zu IRIS-TTS liefern sie in vielen (aber nicht allen) Fällen auch Kartendaten für die dem Verbund zugehörigen Verkehrsmittel. + 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 ÖBB zumeist die beste Wahl.

-- cgit v1.2.3 From 4fb451040f66d26f5bf38e66eb04fb01e6b89adc Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Tue, 25 Mar 2025 18:06:02 +0100 Subject: Account: IRIS is no longer preferred; do not place it first --- lib/Travelynx/Controller/Account.pm | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 0a424cc..e36dcc3 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1070,9 +1070,8 @@ sub backend_form { $backend->{homepage} = 'https://www.bahn.de'; } elsif ( $backend->{dbris} ) { - $type = 'DBRIS'; - $backend->{longname} - = 'Deutsche Bahn: bahn.de'; + $type = 'DBRIS'; + $backend->{longname} = 'Deutsche Bahn: bahn.de'; $backend->{homepage} = 'https://www.bahn.de'; } elsif ( $backend->{hafas} ) { @@ -1141,14 +1140,10 @@ sub backend_form { $backend->{type} = $type; } - my $iris = shift @backends; - @backends = map { $_->[1] } sort { $a->[0] cmp $b->[0] } map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends; - unshift( @backends, $iris ); - $self->render( 'select_backend', suggestions => \@suggested_backends, -- cgit v1.2.3 From ccdfc0206f849ccaeee8f5a5093c51d1274d2652 Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Mon, 9 Jun 2025 13:13:56 +0200 Subject: Add (possibly still somewhat experimental) MOTIS support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squashed commit of the following: commit c7c8b2ec5d8254eefb548bfe7763a7d8c9558be4 Author: Birte Kristina Friesel Date: Mon Jun 9 13:08:57 2025 +0200 fix another merge issue commit d2ae55c901ab59284263ad3070ba425e03cee833 Author: Birte Kristina Friesel Date: Mon Jun 9 13:08:39 2025 +0200 Stations: get_by_external_id is a slow function commit 725174413300e71c350d2f1dcfbeacd751def977 Author: Birte Kristina Friesel Date: Mon Jun 9 13:05:48 2025 +0200 ... I accidentally commited a merge conflict commit c695494dbd6aaf252199da42ad763bdffa1d64b9 Merge: e5da62b 3322ca2 Author: Birte Kristina Friesel Date: Mon Jun 9 12:46:08 2025 +0200 Merge branch 'main' into motis commit e5da62bcfc7953d5109ba53ae1fcc34f509f251b Author: Birte Kristina Friesel Date: Wed Apr 30 18:15:39 2025 +0200 cpanfile: add Travel::Status::MOTIS dependency commit 180723a9e0e2f0aede0bc6352d5eee601183ccef Merge: 479373b c90ae4c Author: Birte Kristina Friesel Date: Wed Apr 30 18:13:45 2025 +0200 Merge branch 'main' into motis commit 479373b14eaadbc022199df246c9fb523a87188c Author: Birte Kristina Friesel Date: Wed Apr 30 18:06:41 2025 +0200 database: remove duplicate users_with_backend migration commit 94c8b5a7d1e2cb7f73b0eca7e33d916775504cd4 Author: Birte Kristina Friesel Date: Wed Apr 30 18:06:04 2025 +0200 Do not store train colours in database. They're only supported by MOTIS. commit d58f23c3c7b06cc0243c1945dacd8673d2d2e428 Author: networkException Date: Fri Apr 18 11:47:02 2025 +0200 Initial MOTIS backend support This patch adds support for checkins using MOTIS backends using the Travel::Status::MOTIS module. With this travelynx supports the two services currently exposed by the module, RNV for local transit in Mannheim, Germany and surrounding cities and transitous for worldwide crowdsourced tranit feeds. This implementation supports realtime predictions, cancellations and polylines as well as custom route colors if available. As MOTIS doesn't expose names of indivial trips currently, displaying transports is mostly limited to route names. MOTIS uses strings for stop ids, based on the used GTFS source feeds. As travelynx's data model currently assumes interger station ids, this patch adds a mapping table to the database. This patch assumes support for MOTIS in db-fakedisplay. Note that while träwelling has migrated to tranitous fully sync remains unsupported for now. See https://github.com/Traewelling/traewelling/issues/3345 --- cpanfile | 1 + lib/Travelynx.pm | 170 ++++++++++++++++++++++++++- lib/Travelynx/Command/database.pm | 214 +++++++++++++++++++++++++++++++++- lib/Travelynx/Command/work.pm | 95 +++++++++++++++ lib/Travelynx/Controller/Account.pm | 47 ++++++++ lib/Travelynx/Controller/Api.pm | 2 + lib/Travelynx/Controller/Traveling.pm | 136 ++++++++++++++++++++- lib/Travelynx/Helper/MOTIS.pm | 158 +++++++++++++++++++++++++ lib/Travelynx/Model/InTransit.pm | 131 ++++++++++++++++++++- lib/Travelynx/Model/Journeys.pm | 28 +++-- lib/Travelynx/Model/Stations.pm | 96 ++++++++++++++- lib/Travelynx/Model/Users.pm | 9 +- public/static/js/geolocation.js | 22 +++- public/static/js/travelynx-actions.js | 24 ++-- sass/src/common/local.scss | 18 +-- templates/_backend_line.html.ep | 2 +- templates/_departures_motis.html.ep | 54 +++++++++ templates/_format_train.html.ep | 4 +- templates/_history_trains.html.ep | 5 +- templates/changelog.html.ep | 14 +++ templates/departures.html.ep | 18 ++- templates/landingpage.html.ep | 2 +- 22 files changed, 1191 insertions(+), 59 deletions(-) create mode 100644 lib/Travelynx/Helper/MOTIS.pm create mode 100644 templates/_departures_motis.html.ep (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/cpanfile b/cpanfile index 85ef852..758acbf 100644 --- a/cpanfile +++ b/cpanfile @@ -17,6 +17,7 @@ requires 'Mojolicious::Plugin::OAuth2'; requires 'Mojo::Pg'; requires 'Text::CSV'; requires 'Text::Markdown'; +requires 'Travel::Status::MOTIS', '>= 0.01'; requires 'Travel::Status::DE::DBRIS', '>= 0.10'; requires 'Travel::Status::DE::HAFAS', '>= 6.20'; requires 'Travel::Status::DE::IRIS'; diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index af46e5a..0429d5e 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -1,6 +1,7 @@ package Travelynx; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -24,6 +25,7 @@ use Travelynx::Helper::DBDB; use Travelynx::Helper::DBRIS; use Travelynx::Helper::HAFAS; use Travelynx::Helper::IRIS; +use Travelynx::Helper::MOTIS; use Travelynx::Helper::Sendmail; use Travelynx::Helper::Traewelling; use Travelynx::Model::InTransit; @@ -259,6 +261,18 @@ sub startup { } ); + $self->helper( + motis => sub { + my ($self) = @_; + state $motis = Travelynx::Helper::MOTIS->new( + log => $self->app->log, + cache => $self->app->cache_iris_rt, + user_agent => $self->ua, + version => $self->app->config->{version}, + ); + } + ); + $self->helper( traewelling => sub { my ($self) = @_; @@ -475,6 +489,9 @@ 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); } @@ -556,6 +573,147 @@ sub startup { } ); + $self->helper( + '_checkin_motis_p' => sub { + my ( $self, %opt ) = @_; + + my $station = $opt{station}; + my $train_id = $opt{train_id}; + 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; + + $self->motis->get_trip_p( + service => $opt{motis}, + trip_id => $train_id, + )->then( + sub { + my ($trip) = @_; + my $found_stopover; + + for my $stopover ( $trip->stopovers ) { + if ( $stopover->stop->id eq $station ) { + $found_stopover = $stopover; + + # Lines may serve the same stop several times. + # Keep looking until the scheduled departure + # matches the one passed while checking in. + if ( $ts and $stopover->scheduled_departure->epoch == $ts ) { + last; + } + } + } + + if ( not $found_stopover ) { + $promise->reject("Did not find stopover at '$station' within trip '$train_id'"); + return; + } + + for my $stopover ( $trip->stopovers ) { + $self->stations->add_or_update( + stop => $stopover->stop, + db => $db, + motis => $opt{motis}, + ); + } + + $self->stations->add_or_update( + stop => $found_stopover->stop, + db => $db, + motis => $opt{motis}, + ); + + eval { + $self->in_transit->add( + uid => $uid, + db => $db, + journey => $trip, + stopover => $found_stopover, + data => { trip_id => $train_id }, + backend_id => $self->stations->get_backend_id( + motis => $opt{motis} + ), + ); + }; + + if ($@) { + $self->app->log->error("Checkin($uid): INSERT failed: $@"); + $promise->reject( 'INSERT failed: ' . $@ ); + return; + } + + my $polyline; + if ( $trip->polyline ) { + my @station_list; + my @coordinate_list; + for my $coordinate ( $trip->polyline ) { + if ( $coordinate->{stop} ) { + if ( not defined $coordinate->{stop}->{eva} ) { + die() + } + + push( + @coordinate_list, + [ + $coordinate->{lon}, $coordinate->{lat}, + $coordinate->{stop}->{eva} + ] + ); + + push( @station_list, $coordinate->{stop}->name ); + } + else { + push( @coordinate_list, [ $coordinate->{lon}, $coordinate->{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 ' + . $trip->route_name + . ' as it only consists of straight lines between stops.' + ); + } + else { + $polyline = { + from_eva => ( $trip->stopovers )[0]->stop->{eva}, + to_eva => ( $trip->stopovers )[-1]->stop->{eva}, + 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($trip); + } + )->catch( + sub { + my ($err) = @_; + $promise->reject($err); + return; + } + )->wait; + + return $promise; + } + ); + $self->helper( '_checkin_dbris_p' => sub { my ( $self, %opt ) = @_; @@ -966,7 +1124,7 @@ sub startup { return $promise->resolve( 0, 'race condition' ); } - if ( $user->{is_dbris} or $user->{is_hafas} ) { + if ( $user->{is_dbris} or $user->{is_hafas} or $user->{is_motis} ) { return $self->_checkout_journey_p(%opt); } @@ -2052,6 +2210,7 @@ sub startup { is_dbris => $latest->{is_dbris}, is_iris => $latest->{is_iris}, is_hafas => $latest->{is_hafas}, + is_motis => $latest->{is_motis}, journey_id => $latest->{journey_id}, timestamp => $action_time, timestamp_delta => $now->epoch - $action_time->epoch, @@ -2063,6 +2222,7 @@ sub startup { real_departure => epoch_to_dt( $latest->{real_dep_ts} ), dep_ds100 => $latest->{dep_ds100}, dep_eva => $latest->{dep_eva}, + dep_external_id => $latest->{dep_external_id}, dep_name => $latest->{dep_name}, dep_lat => $latest->{dep_lat}, dep_lon => $latest->{dep_lon}, @@ -2071,6 +2231,7 @@ sub startup { real_arrival => epoch_to_dt( $latest->{real_arr_ts} ), arr_ds100 => $latest->{arr_ds100}, arr_eva => $latest->{arr_eva}, + arr_external_id => $latest->{arr_external_id}, arr_name => $latest->{arr_name}, arr_lat => $latest->{arr_lat}, arr_lon => $latest->{arr_lon}, @@ -2106,7 +2267,7 @@ sub startup { my $ret = { deprecated => \0, checkedIn => ( - $status->{checked_in} + $status->{checked_in} or $status->{cancelled} ) ? \1 : \0, comment => $status->{comment}, @@ -2114,6 +2275,7 @@ sub startup { id => $status->{backend_id}, type => $status->{is_dbris} ? 'DBRIS' : $status->{is_hafas} ? 'HAFAS' + : $status->{is_motis} ? 'MOTIS' : 'IRIS-TTS', name => $status->{backend_name}, }, @@ -2538,8 +2700,8 @@ sub startup { color => '#673ab7', opacity => @polylines ? $with_polyline - ? 0.4 - : 0.6 + ? 0.4 + : 0.6 : 0.8, }, { diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index d0bc163..0e87b2a 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -1,6 +1,7 @@ package Travelynx::Command::database; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Command'; @@ -11,6 +12,7 @@ use List::Util qw(); use JSON; use Travel::Status::DE::HAFAS; use Travel::Status::DE::IRIS::Stations; +use Travel::Status::MOTIS; has description => 'Initialize or upgrade database layout'; @@ -2854,6 +2856,173 @@ qq{select distinct checkout_station_id from in_transit where backend_id = 0;} } ); }, + + # v61 -> v62 + # Add MOTIS backend type, add RNV and transitous MOTIS backends + sub { + my ($db) = @_; + $db->query( + qq{ + alter table backends add column motis bool default false; + alter table schema_version add column motis varchar(12); + + create table stations_external_ids ( + eva serial not null primary key, + backend_id smallint not null, + external_id text not null, + + unique (backend_id, external_id), + foreign key (eva, backend_id) references stations (eva, source) + ); + + create view stations_with_external_ids as select + stations.*, stations_external_ids.external_id + from stations + left join stations_external_ids on + stations.eva = stations_external_ids.eva and + stations.source = stations_external_ids.backend_id + ; + + drop view in_transit_str; + drop view journeys_str; + drop view users_with_backend; + drop view follows_in_transit; + + create view in_transit_str as select + user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.motis as is_motis, + backend.name as backend_name, in_transit.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + dep_station_external_id.external_id as dep_external_id, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + arr_station_external_id.external_id as arr_external_id, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and in_transit.backend_id = dep_station_external_id.backend_id + left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and in_transit.backend_id = arr_station_external_id.backend_id + left join backends as backend on in_transit.backend_id = backend.id + ; + create view journeys_str as select + journeys.id as journey_id, user_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.motis as is_motis, + backend.name as backend_name, journeys.backend_id as backend_id, + train_type, train_line, train_no, train_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + dep_station_external_id.external_id as dep_external_id, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + arr_station_external_id.external_id as arr_external_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, edited, route, messages, user_data, + dep_platform, arr_platform + from journeys + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source + left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and journeys.backend_id = dep_station_external_id.backend_id + left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and journeys.backend_id = arr_station_external_id.backend_id + left join backends as backend on journeys.backend_id = backend.id + ; + create view users_with_backend as select + users.id as id, users.name as name, status, public_level, + email, password, registered_at, last_seen, + deletion_requested, deletion_notified, use_history, + accept_follows, notifications, profile, backend_id, iris, + hafas, efa, dbris, motis, backend.name as backend_name + from users + left join backends as backend on users.backend_id = backend.id + ; + create view follows_in_transit as select + r1.subject_id as follower_id, user_id as followee_id, + users.name as followee_name, + train_type, train_line, train_no, train_id, + backend.iris as is_iris, backend.hafas as is_hafas, + backend.efa as is_efa, backend.dbris as is_dbris, + backend.motis as is_motis, + backend.name as backend_name, in_transit.backend_id as backend_id, + extract(epoch from checkin_time) as checkin_ts, + extract(epoch from sched_departure) as sched_dep_ts, + extract(epoch from real_departure) as real_dep_ts, + checkin_station_id as dep_eva, + dep_station.ds100 as dep_ds100, + dep_station.name as dep_name, + dep_station.lat as dep_lat, + dep_station.lon as dep_lon, + extract(epoch from checkout_time) as checkout_ts, + extract(epoch from sched_arrival) as sched_arr_ts, + extract(epoch from real_arrival) as real_arr_ts, + checkout_station_id as arr_eva, + arr_station.ds100 as arr_ds100, + arr_station.name as arr_name, + arr_station.lat as arr_lat, + arr_station.lon as arr_lon, + polyline_id, + polylines.polyline as polyline, + visibility, + coalesce(visibility, users.public_level & 127) as effective_visibility, + cancelled, route, messages, user_data, + dep_platform, arr_platform, data + from in_transit + left join polylines on polylines.id = polyline_id + left join users on users.id = user_id + left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id + left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source + left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source + left join backends as backend on in_transit.backend_id = backend.id + order by checkin_time desc + ; + } + ); + $db->query( + qq{ + update schema_version set version = 62; + } + ); + }, ); sub sync_stations { @@ -3044,7 +3213,7 @@ sub sync_stations { } } -sub sync_backends { +sub sync_backends_hafas { my ($db) = @_; for my $service ( Travel::Status::DE::HAFAS::get_services() ) { my $present = $db->select( @@ -3074,6 +3243,36 @@ sub sync_backends { { hafas => $Travel::Status::DE::HAFAS::VERSION } ); } +sub sync_backends_motis { + my ($db) = @_; + for my $service ( Travel::Status::MOTIS::get_services() ) { + my $present = $db->select( + 'backends', + 'count(*) as count', + { + motis => 1, + name => $service->{shortname} + } + )->hash->{count}; + if ( not $present ) { + $db->insert( + 'backends', + { + iris => 0, + hafas => 0, + efa => 0, + dbris => 0, + motis => 1, + name => $service->{shortname}, + }, + { on_conflict => undef } + ); + } + } + + $db->update( 'schema_version', { motis => $Travel::Status::MOTIS::VERSION } ); +} + sub setup_db { my ($db) = @_; my $tx = $db->begin; @@ -3169,7 +3368,18 @@ sub migrate_db { else { say "Synchronizing with Travel::Status::DE::HAFAS $Travel::Status::DE::HAFAS::VERSION"; - sync_backends($db); + sync_backends_hafas($db); + } + + my $motis_version = get_schema_version( $db, 'motis' ) // '0'; + say "Found backend table for Motis v${motis_version}"; + if ( $motis_version eq $Travel::Status::MOTIS::VERSION ) { + say 'Backend table is up-to-date'; + } + else { + say +"Synchronizing with Travel::Status::MOTIS $Travel::Status::MOTIS::VERSION"; + sync_backends_motis($db); } $db->update( 'schema_version', diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm index 4876e98..4ff5c9e 100644 --- a/lib/Travelynx/Command/work.pm +++ b/lib/Travelynx/Command/work.pm @@ -1,6 +1,7 @@ package Travelynx::Command::work; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Command'; @@ -184,6 +185,100 @@ sub run { next; } + if ( $entry->{is_motis} ) { + + eval { + $self->app->motis->trip_id( + service => $entry->{backend_name}, + trip_id => $train_id, + )->then( + sub { + my ($journey) = @_; + + for my $stopover ( $journey->stopovers ) { + if ( not defined $stopover->stop->{eva} ) { + my $stop = $self->app->stations->get_by_external_id( + external_id => $stopover->stop->id, + motis => $entry->{backend_name}, + ); + + $stopover->stop->{eva} = $stop->{eva}; + } + } + + my $found_departure; + my $found_arrival; + for my $stopover ( $journey->stopovers ) { + if ( $stopover->stop->{eva} == $dep ) { + $found_departure = $stopover; + } + + if ( $arr and $stopover->stop->{eva} == $arr ) { + $found_arrival = $stopover; + last; + } + } + + if ( not $found_departure ) { + $self->app->log->debug("Did not find $dep within trip $train_id"); + return; + } + + if ( $found_departure->realtime_departure ) { + $self->app->in_transit->update_departure_motis( + uid => $uid, + journey => $journey, + stopover => $found_departure, + dep_eva => $dep, + arr_eva => $arr, + train_id => $train_id, + ); + } + + if ( $found_arrival and $found_arrival->realtime_arrival ) { + $self->app->in_transit->update_arrival_motis( + uid => $uid, + journey => $journey, + train_id => $train_id, + stopover => $found_arrival, + dep_eva => $dep, + arr_eva => $arr + ); + } + } + )->catch( + sub { + my ($err) = @_; + $self->app->log->error( +"work($uid) @ MOTIS $entry->{backend_name}: journey: $err" + ); + if ( $err =~ m{HTTP 429} ) { + $dbris_rate_limited = 1; + } + } + )->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) @ MOTIS $entry->{backend_name}: $@"); + } + next; + } + if ( $entry->{is_hafas} ) { eval { diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index e36dcc3..4c69f91 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1,6 +1,7 @@ package Travelynx::Controller::Account; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Controller'; @@ -1137,6 +1138,52 @@ sub backend_form { $type = undef; } } + elsif ( $backend->{motis} ) { + my $s = $self->motis->get_service( $backend->{name} ); + + $type = 'MOTIS'; + $backend->{longname} = $s->{name}; + $backend->{homepage} = $s->{homepage}; + $backend->{regions} = [ map { $place_map{$_} // $_ } + @{ $s->{coverage}{regions} // [] } ]; + $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + + if ( $backend->{name} eq 'transitous' ) { + $backend->{regions} = [ 'Weltweit' ]; + } + if ( $backend->{name} eq 'RNV' ) { + $backend->{homepage} = 'https://rnv-online.de/'; + } + + 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; + } + } + } + } $backend->{type} = $type; } diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm index 9fe72b2..572d3fa 100755 --- a/lib/Travelynx/Controller/Api.pm +++ b/lib/Travelynx/Controller/Api.pm @@ -189,6 +189,7 @@ sub travel_v1 { my $train_id; my $dbris = sanitize( undef, $payload->{dbris} ); my $hafas = sanitize( undef, $payload->{hafas} ); + my $motis = sanitize( undef, $payload->{motis} ); if ( not $hafas and exists $payload->{train}{journeyID} ) { $dbris //= 'bahn.de'; @@ -298,6 +299,7 @@ sub travel_v1 { uid => $uid, hafas => $hafas, dbris => $dbris, + motis => $motis, ); } )->then( diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 0d89fb9..aa7ee9b 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -1,6 +1,7 @@ package Travelynx::Controller::Traveling; # Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2025 networkException # # SPDX-License-Identifier: AGPL-3.0-or-later use Mojo::Base 'Mojolicious::Controller'; @@ -48,6 +49,11 @@ sub get_connecting_trains_p { # cases. But not reliably. Probably best to leave it out entirely then. return $promise->reject; } + if ( $user->{backend_motis} ) { + + # FIXME: The following code can't handle external_ids currently + return $promise->reject; + } if ( $opt{eva} ) { if ( $use_history & 0x01 ) { @@ -408,8 +414,8 @@ sub homepage { } } else { - @recent_targets = uniq_by { $_->{eva} } - $self->journeys->get_latest_checkout_stations( uid => $uid ); + @recent_targets = uniq_by { $_->{external_id_or_eva} } + $self->journeys->get_latest_checkout_stations( uid => $uid ); } $self->render( 'landingpage', @@ -550,7 +556,7 @@ sub geolocation { return; } - my ( $dbris_service, $hafas_service ); + my ( $dbris_service, $hafas_service, $motis_service ); my $backend = $self->stations->get_backend( backend_id => $backend_id ); if ( $backend->{dbris} ) { $dbris_service = $backend->{name}; @@ -558,6 +564,9 @@ sub geolocation { elsif ( $backend->{hafas} ) { $hafas_service = $backend->{name}; } + elsif ( $backend->{motis} ) { + $motis_service = $backend->{name}; + } if ($dbris_service) { $self->render_later; @@ -654,6 +663,54 @@ sub geolocation { return; } + elsif ($motis_service) { + $self->render_later; + + Travel::Status::MOTIS->new_p( + promise => 'Mojo::Promise', + user_agent => $self->ua, + + service => $motis_service, + stops_by_coordinate => { + lat => $lat, + lon => $lon + } + )->then( + sub { + my ($motis) = @_; + my @motis = map { + { + id => $_->id, + name => $_->name, + distance => 0, + motis => $motis_service, + } + } $motis->results; + + if ( @motis > 10 ) { + @motis = @motis[ 0 .. 9 ]; + } + + $self->render( + json => { + candidates => [@motis], + } + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + json => { + candidates => [], + warning => $err, + } + ); + } + )->wait; + + return; + } my @iris = map { { @@ -734,6 +791,7 @@ sub travel_action { return $self->checkin_p( dbris => $params->{dbris}, hafas => $params->{hafas}, + motis => $params->{motis}, station => $params->{station}, train_id => $params->{train}, train_suffix => $params->{suffix}, @@ -872,6 +930,13 @@ sub travel_action { . '?hafas=' . $status->{backend_name}; } + elsif ( $status->{is_motis} ) { + $redir + = '/s/' + . $status->{dep_external_id} + . '?motis=' + . $status->{backend_name}; + } else { $redir = '/s/' . $status->{dep_ds100}; } @@ -889,6 +954,7 @@ sub travel_action { $self->checkin_p( dbris => $params->{dbris}, hafas => $params->{hafas}, + motis => $params->{motis}, station => $params->{station}, train_id => $params->{train}, ts => $params->{ts}, @@ -1021,6 +1087,8 @@ sub station { // ( $user->{backend_dbris} ? $user->{backend_name} : undef ); my $hafas_service = $self->param('hafas') // ( $user->{backend_hafas} ? $user->{backend_name} : undef ); + my $motis_service = $self->param('motis') + // ( $user->{backend_motis} ? $user->{backend_name} : undef ); my $promise; if ($dbris_service) { if ( $station !~ m{ [@] L = \d+ }x ) { @@ -1053,6 +1121,35 @@ sub station { lookahead => 30, ); } + elsif ($motis_service) { + if ( $station !~ m/.*_.*/ ) { + $self->render_later; + $self->motis->get_station_by_query_p( + service => $motis_service, + query => $station, + )->then( + sub { + my ($motis_station) = @_; + $self->redirect_to( '/s/' . $motis_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + say "$err"; + + $self->redirect_to('/'); + } + )->wait; + return; + } + $promise = $self->motis->get_departures_p( + service => $motis_service, + station_id => $station, + timestamp => $timestamp, + lookbehind => 30, + lookahead => 30, + ) + } else { $promise = $self->iris->get_departures_p( station => $station, @@ -1106,6 +1203,17 @@ sub station { related_stations => [], }; } + elsif ($motis_service) { + @results = map { $_->[0] } + sort { $b->[1] <=> $a->[1] } + map { [ $_, $_->stopover->departure->epoch ] } $status->results; + + $status = { + station_eva => $station, + station_name => $status->{results}->[0]->stopover->stop->name, + related_stations => [], + }; + } else { # You can't check into a train which terminates here @@ -1172,6 +1280,7 @@ sub station { user => $user, dbris => $dbris_service, hafas => $hafas_service, + motis => $motis_service, eva => $status->{station_eva}, datetime => $timestamp, now_in_range => $now_within_range, @@ -1192,6 +1301,7 @@ sub station { user => $user, dbris => $dbris_service, hafas => $hafas_service, + motis => $motis_service, eva => $status->{station_eva}, datetime => $timestamp, now_in_range => $now_within_range, @@ -1211,6 +1321,7 @@ sub station { user => $user, dbris => $dbris_service, hafas => $hafas_service, + motis => $motis_service, eva => $status->{station_eva}, datetime => $timestamp, now_in_range => $now_within_range, @@ -1322,6 +1433,23 @@ sub redirect_to_station { } )->wait; } + elsif ( $self->param('backend_motis') ) { + $self->render_later; + $self->motis->get_station_by_query( + service => $self->param('backend_motis'), + query => $station, + )->then( + sub { + my ($motis_station) = @_; + $self->redirect_to( '/s/' . $motis_station->{id} ); + } + )->catch( + sub { + my ($err) = @_; + $self->redirect_to('/'); + } + )->wait; + } else { $self->redirect_to("/s/${station}"); } @@ -1892,7 +2020,7 @@ sub journey_details { $delay = sprintf( 'mit %+d ', ( - $journey->{rt_arrival}->epoch + $journey->{rt_arrival}->epoch - $journey->{sched_arrival}->epoch ) / 60 ); diff --git a/lib/Travelynx/Helper/MOTIS.pm b/lib/Travelynx/Helper/MOTIS.pm new file mode 100644 index 0000000..ee2b10b --- /dev/null +++ b/lib/Travelynx/Helper/MOTIS.pm @@ -0,0 +1,158 @@ +package Travelynx::Helper::MOTIS; + +# Copyright (C) 2025 networkException +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +use strict; +use warnings; +use 5.020; +use utf8; + +use DateTime; +use Encode qw(decode); +use JSON; +use Mojo::Promise; +use Mojo::UserAgent; + +use Travel::Status::MOTIS; + +sub _epoch { + my ($dt) = @_; + + return $dt ? $dt->epoch : 0; +} + +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::MOTIS::get_service($service); +} + +sub get_station_by_query_p { + my ( $self, %opt ) = @_; + + $opt{service} //= 'transitous'; + + my $promise = Mojo::Promise->new; + + Travel::Status::MOTIS->new_p( + cache => $self->{cache}, + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + lwp_options => { + timeout => 10, + agent => $self->{header}{'User-Agent'}, + }, + + service => $opt{service}, + stops_by_query => $opt{query}, + )->then( + sub { + my ($motis) = @_; + my $found; + + for my $result ( $motis->results ) { + if ( defined $result->id ) { + $promise->resolve($result); + return; + } + } + + $promise->reject("Unable to find station '$opt{query}'"); + return; + } + )->catch( + sub { + my ($err) = @_; + $promise->reject("'$err' while trying to look up '$opt{query}'"); + return; + } + )->wait; + + return $promise; +} + +sub get_departures_p { + my ( $self, %opt ) = @_; + + $opt{service} //= 'transitous'; + + my $timestamp = ( + $opt{timestamp} + ? $opt{timestamp}->clone + : DateTime->now + )->subtract( minutes => $opt{lookbehind} ); + + return Travel::Status::MOTIS->new_p( + cache => $self->{cache}, + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + lwp_options => { + timeout => 10, + agent => $self->{header}{'User-Agent'}, + }, + + service => $opt{service}, + timestamp => $timestamp, + stop_id => $opt{station_id}, + results => 60, + ); +} + +sub get_trip_p { + my ( $self, %opt ) = @_; + + $opt{service} //= 'transitous'; + + my $promise = Mojo::Promise->new; + + Travel::Status::MOTIS->new_p( + with_polyline => $opt{with_polyline}, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + + service => $opt{service}, + trip_id => $opt{trip_id}, + )->then( + sub { + my ($motis) = @_; + my $journey = $motis->result; + + if ($journey) { + $self->{log}->debug("get_trip_p($opt{trip_id}): success"); + $promise->resolve($journey); + return; + } + + $self->{log}->debug("get_trip_p($opt{trip_id}): no journey"); + $promise->reject('no journey'); + return; + } + )->catch( + sub { + my ($err) = @_; + $self->{log}->debug("get_trip_p($opt{trip_id}): error $err"); + $promise->reject($err); + return; + } + )->wait; + + return $promise; +} + +1; diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm index eeb1d87..0e3fdc6 100644 --- a/lib/Travelynx/Model/InTransit.pm +++ b/lib/Travelynx/Model/InTransit.pm @@ -1,6 +1,7 @@ package Travelynx::Model::InTransit; # Copyright (C) 2020-2025 Birte Kristina Friesel +# Copyright (C) 2025 networkException # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -99,6 +100,7 @@ sub add { my $train_suffix = $opt{train_suffix}; my $journey = $opt{journey}; my $stop = $opt{stop}; + my $stopover = $opt{stopover}; my $checkin_station_id = $opt{departure_eva}; my $route = $opt{route}; my $data = $opt{data}; @@ -282,6 +284,57 @@ sub add { } ); } + elsif ( $journey and $stopover ) { + + # MOTIS + my @route; + for my $journey_stopover ( $journey->stopovers ) { + push( + @route, + [ + $journey_stopover->stop->name, + $journey_stopover->stop->{eva} // die('eva not set for stopover'), + { + sched_arr => _epoch( $journey_stopover->scheduled_arrival ), + sched_dep => _epoch( $journey_stopover->scheduled_departure ), + rt_arr => _epoch( $journey_stopover->realtime_arrival ), + rt_dep => _epoch( $journey_stopover->realtime_departure ), + arr_delay => $journey_stopover->arrival_delay, + dep_delay => $journey_stopover->departure_delay, + lat => $journey_stopover->stop->lat, + lon => $journey_stopover->stop->lon, + } + ] + ); + } + + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => $stopover->{is_cancelled} + ? 1 + : 0, + checkin_station_id => $stopover->stop->{eva}, + checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ), + dep_platform => $stopover->track, + train_type => $journey->mode, + train_no => q{}, + train_id => $journey->id, + train_line => $journey->route_name, + sched_departure => $stopover->scheduled_departure, + real_departure => $stopover->departure, + route => $json->encode( \@route ), + data => JSON->new->encode( + { + rt => $stopover->{is_realtime} ? 1 : 0, + %{ $data // {} } + } + ), + backend_id => $backend_id, + } + ); + } else { die('neither train nor journey specified'); } @@ -331,7 +384,7 @@ sub postprocess { # Note that the departure stop may be present more than once in @route, # e.g. when traveling along ring lines such as S41 / S42 in Berlin. if ( - $ret->{dep_name} + $ret->{dep_name} and $station->[0] eq $ret->{dep_name} and not($station->[2]{sched_dep} and $station->[2]{sched_dep} < $ret->{sched_dep_ts} ) @@ -887,6 +940,33 @@ sub update_departure_dbris { ); } +sub update_departure_motis { + 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 $stopover = $opt{stopover}; + my $json = JSON->new; + + # 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', + { + real_departure => $stopover->{realtime_departure}, + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_departure_hafas { my ( $self, %opt ) = @_; my $uid = $opt{uid}; @@ -1053,6 +1133,55 @@ sub update_arrival_dbris { ); } +sub update_arrival_motis { + 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 $stopover = $opt{stopover}; + my $json = JSON->new; + + my @route; + for my $journey_stopover ( $journey->stopovers ) { + push( + @route, + [ + $journey_stopover->stop->name, + $journey_stopover->stop->{eva} // die('eva not set for stopover'), + { + sched_arr => _epoch( $journey_stopover->scheduled_arrival ), + sched_dep => _epoch( $journey_stopover->scheduled_departure ), + rt_arr => _epoch( $journey_stopover->realtime_arrival ), + rt_dep => _epoch( $journey_stopover->realtime_departure ), + arr_delay => $journey_stopover->arrival_delay, + dep_delay => $journey_stopover->departure_delay, + lat => $journey_stopover->stop->lat, + lon => $journey_stopover->stop->lon, + } + ] + ); + } + + # 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', + { + real_arrival => $stopover->{realtime_arrival}, + route => $json->encode( [@route] ), + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_arrival_hafas { my ( $self, %opt ) = @_; my $uid = $opt{uid}; diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index f5bc9f1..fff59f9 100755 --- a/lib/Travelynx/Model/Journeys.pm +++ b/lib/Travelynx/Model/Journeys.pm @@ -549,7 +549,7 @@ sub get { my @select = ( - qw(journey_id is_dbris is_iris is_hafas backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility) + qw(journey_id is_dbris is_iris is_hafas is_motis backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility) ); my %where = ( user_id => $uid, @@ -610,6 +610,7 @@ sub get { is_dbris => $entry->{is_dbris}, is_iris => $entry->{is_iris}, is_hafas => $entry->{is_hafas}, + is_motis => $entry->{is_motis}, backend_name => $entry->{backend_name}, backend_id => $entry->{backend_id}, type => $entry->{train_type}, @@ -871,8 +872,9 @@ sub get_latest_checkout_stations { my $res = $db->select( 'journeys_str', [ - 'arr_name', 'arr_eva', 'train_id', 'backend_id', - 'backend_name', 'is_dbris', 'is_hafas' + 'arr_name', 'arr_eva', 'arr_external_id', 'train_id', + 'backend_id', 'backend_name', 'is_dbris', 'is_hafas', + 'is_motis' ], { user_id => $uid, @@ -894,11 +896,13 @@ sub get_latest_checkout_stations { push( @ret, { - name => $row->{arr_name}, - eva => $row->{arr_eva}, - dbris => $row->{is_dbris} ? $row->{backend_name} : 0, - hafas => $row->{is_hafas} ? $row->{backend_name} : 0, - backend_id => $row->{backend_id}, + name => $row->{arr_name}, + eva => $row->{arr_eva}, + external_id_or_eva => $row->{arr_external_id} // $row->{arr_eva}, + dbris => $row->{is_dbris} ? $row->{backend_name} : 0, + hafas => $row->{is_hafas} ? $row->{backend_name} : 0, + motis => $row->{is_motis} ? $row->{backend_name} : 0, + backend_id => $row->{backend_id}, } ); } @@ -1392,7 +1396,7 @@ sub compute_review { if ( not $most_undelay or $speedup > ( - $most_undelay->{sched_duration} + $most_undelay->{sched_duration} - $most_undelay->{rt_duration} ) ) @@ -1665,7 +1669,7 @@ sub compute_stats { @inconsistencies, { conflict => { - train => $journey->{type} . ' ' + train => ( $journey->{is_motis} ? '' : $journey->{type} ) . ' ' . ( $journey->{line} // $journey->{no} ), arr => epoch_to_dt( $journey->{rt_arr_ts} ) ->strftime('%d.%m.%Y %H:%M'), @@ -1691,7 +1695,7 @@ sub compute_stats { $next_departure = $journey->{rt_dep_ts}; $next_id = $journey->{id}; $next_train - = $journey->{type} . ' ' . ( $journey->{line} // $journey->{no} ),; + = ( $journey->{is_motis} ? '' : $journey->{type} ) . ' ' . ( $journey->{line} // $journey->{no} ),; } my $ret = { km_route => $km_route, @@ -1740,7 +1744,7 @@ sub get_stats { # checks out of a train or manually edits/adds a journey. if ( - not $opt{write_only} + not $opt{write_only} and not $opt{review} and my $stats = $self->stats_cache->get( uid => $uid, diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm index 3d6549f..761c5de 100644 --- a/lib/Travelynx/Model/Stations.pm +++ b/lib/Travelynx/Model/Stations.pm @@ -1,6 +1,7 @@ package Travelynx::Model::Stations; # Copyright (C) 2022 Birte Kristina Friesel +# Copyright (C) 2025 networkException # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -28,6 +29,9 @@ sub get_backend_id { if ( $opt{dbris} and $self->{backend_id}{dbris}{ $opt{dbris} } ) { return $self->{backend_id}{dbris}{ $opt{dbris} }; } + if ( $opt{motis} and $self->{backend_id}{motis}{ $opt{motis} } ) { + return $self->{backend_id}{motis}{ $opt{motis} }; + } my $db = $opt{db} // $self->{pg}->db; my $backend_id = 0; @@ -54,6 +58,17 @@ sub get_backend_id { )->hash->{id}; $self->{backend_id}{hafas}{ $opt{hafas} } = $backend_id; } + elsif ( $opt{motis} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + motis => 1, + name => $opt{motis} + } + )->hash->{id}; + $self->{backend_id}{motis}{ $opt{motis} } = $backend_id; + } return $backend_id; } @@ -85,7 +100,7 @@ sub get_backends { $opt{db} //= $self->{pg}->db; my $res = $opt{db} - ->select( 'backends', [ 'id', 'name', 'iris', 'hafas', 'dbris' ] ); + ->select( 'backends', [ 'id', 'name', 'iris', 'hafas', 'dbris', 'motis' ] ); my @ret; while ( my $row = $res->hash ) { @@ -97,6 +112,7 @@ sub get_backends { iris => $row->{iris}, dbris => $row->{dbris}, hafas => $row->{hafas}, + motis => $row->{motis}, } ); } @@ -149,6 +165,61 @@ sub add_or_update { return; } + if ( $opt{motis} ) { + if ( + my $s = $self->get_by_external_id( + external_id => $stop->id, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( + 'stations', + { + name => $stop->name, + lat => $stop->lat, + lon => $stop->lon, + archived => 0 + }, + { + eva => $s->{eva}, + source => $opt{backend_id} + } + ); + + $stop->{eva} = $s->{eva}; + + return; + } + + my $s = $opt{db}->query( + qq { + with new_station as ( + insert into stations_external_ids (backend_id, external_id) + values (?, ?) + returning eva, backend_id + ) + + insert into stations (eva, name, lat, lon, source, archived) + values ((select eva from new_station), ?, ?, ?, (select backend_id from new_station), ?) + returning * + }, + ( + $opt{backend_id}, + $stop->id, + $stop->name, + $stop->lat, + $stop->lon, + 0, + ) + ); + + $stop->{eva} = $s->hash->{eva}; + + return; + } + my $loc = $stop->loc; if ( my $s = $self->get_by_eva( @@ -184,6 +255,8 @@ sub add_or_update { archived => 0 } ); + + return; } sub add_meta { @@ -276,6 +349,27 @@ sub get_by_eva { )->hash; } +# Slow +sub get_by_external_id { + my ( $self, %opt ) = @_; + + if ( not $opt{external_id} ) { + return; + } + + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); + + return $opt{db}->select( + 'stations_with_external_ids', + '*', + { + external_id => $opt{external_id}, + source => $opt{backend_id}, + } + )->hash; +} + # Fast sub get_by_evas { my ( $self, %opt ) = @_; diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 750e889..10ab17e 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', + . 'backend_id, backend_name, hafas, dbris, motis', { id => $uid } )->hash; if ($user) { @@ -459,6 +459,7 @@ sub get { backend_name => $user->{backend_name}, backend_dbris => $user->{dbris}, backend_hafas => $user->{hafas}, + backend_motis => $user->{motis}, }; } return undef; @@ -1026,11 +1027,11 @@ sub get_followers { id => $row->{id}, name => $row->{name}, following_back => ( - $row->{inverse_predicate} + $row->{inverse_predicate} and $row->{inverse_predicate} == $predicate_atoi{follows} ) ? 1 : 0, followback_requested => ( - $row->{inverse_predicate} + $row->{inverse_predicate} and $row->{inverse_predicate} == $predicate_atoi{requests_follow} ) ? 1 : 0, @@ -1102,7 +1103,7 @@ sub get_followees { id => $row->{id}, name => $row->{name}, following_back => ( - $row->{inverse_predicate} + $row->{inverse_predicate} and $row->{inverse_predicate} == $predicate_atoi{follows} ) ? 1 : 0, } diff --git a/public/static/js/geolocation.js b/public/static/js/geolocation.js index c428acd..1bb4b2b 100644 --- a/public/static/js/geolocation.js +++ b/public/static/js/geolocation.js @@ -24,7 +24,9 @@ $(document).ready(function() { const res = $(document.createElement('p')); $.each(stops, function(i, stop) { const parts = stop.split(';'); - const node = $('' + parts[1] + ''); + const [ eva, name, dbris, motis, hafas ] = parts; + + const node = $('' + name + ''); node.click(function() { $('nav .preloader-wrapper').addClass('active'); }); @@ -45,13 +47,21 @@ $(document).ready(function() { } else { const res = $(document.createElement('p')); $.each(data.candidates, function(i, candidate) { + let node; + + if (candidate.motis !== undefined) { + const { id, name, motis } = candidate; + + node = $('' + name + ''); + } else { + const eva = candidate.eva, + name = candidate.name, + hafas = candidate.hafas, + distance = candidate.distance.toFixed(1); - const eva = candidate.eva, - name = candidate.name, - hafas = candidate.hafas, - distance = candidate.distance.toFixed(1); + node = $('' + name + ''); + } - const node = $('' + name + ''); node.click(function() { $('nav .preloader-wrapper').addClass('active'); }); diff --git a/public/static/js/travelynx-actions.js b/public/static/js/travelynx-actions.js index 3e02283..d2316af 100644 --- a/public/static/js/travelynx-actions.js +++ b/public/static/js/travelynx-actions.js @@ -196,6 +196,7 @@ function tvly_reg_handlers() { action: 'checkin', dbris: link.data('dbris'), hafas: link.data('hafas'), + motis: link.data('motis'), station: link.data('station'), train: link.data('train'), suffix: link.data('suffix'), @@ -210,6 +211,7 @@ function tvly_reg_handlers() { action: 'checkout', dbris: link.data('dbris'), hafas: link.data('hafas'), + motis: link.data('motis'), station: link.data('station'), force: link.data('force'), }; @@ -242,6 +244,7 @@ function tvly_reg_handlers() { action: 'cancelled_from', dbris: link.data('dbris'), hafas: link.data('hafas'), + motis: link.data('motis'), station: link.data('station'), ts: link.data('ts'), train: link.data('train'), @@ -254,6 +257,7 @@ function tvly_reg_handlers() { action: 'cancelled_to', dbris: link.data('dbris'), hafas: link.data('hafas'), + motis: link.data('motis'), station: link.data('station'), force: true, }; @@ -320,18 +324,18 @@ $(document).ready(function() { $('nav .preloader-wrapper').addClass('active'); }); $('a[href="#now"]').keydown(function(event) { - // also trigger click handler on keyboard enter - if (event.keyCode == 13) { - event.preventDefault(); - event.target.click(); - } + // also trigger click handler on keyboard enter + if (event.keyCode == 13) { + event.preventDefault(); + event.target.click(); + } }); $('a[href="#now"]').click(function(event) { - event.preventDefault(); - $('nav .preloader-wrapper').removeClass('active'); - now_el = $('#now')[0]; - now_el.previousElementSibling.querySelector(".dep-time").focus(); - now_el.scrollIntoView({behavior: "smooth", block: "center"}); + event.preventDefault(); + $('nav .preloader-wrapper').removeClass('active'); + now_el = $('#now')[0]; + now_el.previousElementSibling.querySelector(".dep-time").focus(); + now_el.scrollIntoView({behavior: "smooth", block: "center"}); }); const elems = document.querySelectorAll('.carousel'); const instances = M.Carousel.init(elems, { diff --git a/sass/src/common/local.scss b/sass/src/common/local.scss index 605ca76..2ba0ffa 100644 --- a/sass/src/common/local.scss +++ b/sass/src/common/local.scss @@ -209,30 +209,30 @@ ul.route-history > li { min-width: 6ch; margin: 0 auto; - &.Bus, &.RUF, &.AST { + &.Bus, &.BUS, &.RUF, &.AST { background-color: #a3167e; border-radius: 5rem; padding: .2rem .5rem; } - &.STR, &.Tram, &.Str, &.Strb, &.STB { + &.STR, &.Tram, &.TRAM, &.Str, &.Strb, &.STB { background-color: #c5161c; border-radius: 5rem; padding: .2rem .5rem; } - &.S, &.RS, &.RER, &.SKW { + &.S, &.RS, &.RER, &.SKW, &.METRO { background-color: #008d4f; border-radius: 5rem; padding: .2rem .5rem; } - &.U, &.M { + &.U, &.M, &.SUBWAY { background-color: #014e8d; border-radius: 5rem; padding: .2rem .5rem; } - &.RE, &.IRE, &.REX { + &.RE, &.IRE, &.REX, &.REGIONAL_FAST_RAIL { background-color: #ff4f00; } - &.RB, &.MEX, &.TER, &.R { + &.RB, &.MEX, &.TER, &.R, &.REGIONAL_RAIL { background-color: #1f4a87; } // DE @@ -242,7 +242,9 @@ ul.route-history > li { // FR &.TGV, &.OGV, &.EST, // PL - &.TLK, &.EIC { + &.TLK, &.EIC, + // MOTIS + &.HIGHSPEED_RAIL, &.LONG_DISTANCE { background-color: #ff0404; font-weight: 900; font-style: italic; @@ -251,7 +253,7 @@ ul.route-history > li { &.RJ, &.RJX { background-color: #c63131; } - &.NJ, &.EN { + &.NJ, &.EN, &.NIGHT_RAIL { background-color: #29255b; } &.WB { diff --git a/templates/_backend_line.html.ep b/templates/_backend_line.html.ep index 5f2bcf1..00496d3 100644 --- a/templates/_backend_line.html.ep +++ b/templates/_backend_line.html.ep @@ -6,7 +6,7 @@ % } % if ($backend->{has_area}) {
- <%= join(q{, }, @{$backend->{regions} // []}) || '[Karte]' %> + <%= join(q{, }, @{$backend->{regions} // []}) || '[Karte]' %> % } % elsif ($backend->{regions}) {
diff --git a/templates/_departures_motis.html.ep b/templates/_departures_motis.html.ep new file mode 100644 index 0000000..2ebc5de --- /dev/null +++ b/templates/_departures_motis.html.ep @@ -0,0 +1,54 @@ + diff --git a/templates/_format_train.html.ep b/templates/_format_train.html.ep index 1d6acaa..e82f3f9 100644 --- a/templates/_format_train.html.ep +++ b/templates/_format_train.html.ep @@ -2,7 +2,9 @@ 🏳️‍🌈 % } - <%= $journey->{train_type} %> + % if (not $journey->{is_motis}) { + <%= $journey->{train_type} %> + % } <%= $journey->{train_line} // $journey->{train_no}%> % if ($journey->{train_line}) { diff --git a/templates/_history_trains.html.ep b/templates/_history_trains.html.ep index cf998ab..7ae2a1d 100644 --- a/templates/_history_trains.html.ep +++ b/templates/_history_trains.html.ep @@ -17,7 +17,10 @@
  • - <%= $travel->{type} %> <%= $travel->{line} // $travel->{no}%> + % if (not $travel->{is_motis}) { + <%= $travel->{type} %> + % } + <%= $travel->{line} // $travel->{no}%> diff --git a/templates/changelog.html.ep b/templates/changelog.html.ep index 7a1417f..73eae7b 100644 --- a/templates/changelog.html.ep +++ b/templates/changelog.html.ep @@ -1,5 +1,19 @@

    Changelog

    +
    +
    + 2.13 +
    +
    +

    + add + Experimentelle Unterstützung für Checkins via MOTIS-Backends + (derzeit transitous und RNV). Vielen Dank an networkException + für die Implementierung der API und Einbindung in travelynx. +

    +
    +
    +
    2.12 diff --git a/templates/departures.html.ep b/templates/departures.html.ep index bbae40f..1745a47 100644 --- a/templates/departures.html.ep +++ b/templates/departures.html.ep @@ -15,6 +15,9 @@ % elsif (param('hafas')) { <%= param('hafas') %> % } + % elsif (param('motis')) { + <%= param('motis') %> + % } % else { % if ($user->{backend_id}) { <%= $user->{backend_name} %> @@ -33,7 +36,13 @@
    Aktuell eingecheckt -

    In <%= $user_status->{train_type} %> <%= $user_status->{train_no} %> +

    In + % if ( not $user_status->{is_motis} ) { + <%= $user_status->{train_type} %> + % } + + <%= $user_status->{train_line} // $user_status->{train_no} %> + % if ( $user_status->{arr_name}) { von <%= $user_status->{dep_name} %> nach <%= $user_status->{arr_name} %> % } @@ -96,7 +105,7 @@

    - % if ($dbris or $hafas) { + % if ($dbris or $hafas or $motis) { früher % }
    @@ -106,7 +115,7 @@ % }
    - % if ($dbris or $hafas) { + % if ($dbris or $hafas or $motis) { später % }
    @@ -154,6 +163,9 @@ % elsif ($hafas) { %= include '_departures_hafas', results => $results, hafas => $hafas; % } + % elsif ($motis) { + %= include '_departures_motis', results => $results, motis => $motis; + % } % else { %= include '_departures_iris', results => $results; % } diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep index 67ba806..56aa8ff 100644 --- a/templates/landingpage.html.ep +++ b/templates/landingpage.html.ep @@ -57,7 +57,7 @@
    Hallo, <%= $user->{name} %>!

    Du bist gerade nicht eingecheckt.

    -
    + %= hidden_field backend_dbris => $user->{backend_dbris} -- cgit v1.2.3 From 0d3e16487861ca644920b7bacf1db0bac3c63199 Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Mon, 9 Jun 2025 13:31:43 +0200 Subject: perltidy --- lib/Travelynx.pm | 38 +++++++++++++++++++++------------ lib/Travelynx/Command/database.pm | 3 ++- lib/Travelynx/Command/traewelling.pm | 4 ++-- lib/Travelynx/Command/work.pm | 12 +++++++---- lib/Travelynx/Controller/Account.pm | 13 ++++++----- lib/Travelynx/Controller/Traewelling.pm | 2 +- lib/Travelynx/Controller/Traveling.pm | 18 +++++++++------- lib/Travelynx/Helper/DBDB.pm | 21 ++++++++++-------- lib/Travelynx/Helper/MOTIS.pm | 20 ++++++++--------- lib/Travelynx/Helper/Traewelling.pm | 18 +++++++++------- lib/Travelynx/Model/InTransit.pm | 34 +++++++++++++++++------------ lib/Travelynx/Model/Journeys.pm | 29 +++++++++++++++---------- lib/Travelynx/Model/Stations.pm | 12 ++++------- lib/Travelynx/Model/Users.pm | 6 +++--- 14 files changed, 130 insertions(+), 100 deletions(-) (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index 0429d5e..529f7fe 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -601,14 +601,19 @@ sub startup { # Lines may serve the same stop several times. # Keep looking until the scheduled departure # matches the one passed while checking in. - if ( $ts and $stopover->scheduled_departure->epoch == $ts ) { + if ( $ts + and $stopover->scheduled_departure->epoch + == $ts ) + { last; } } } if ( not $found_stopover ) { - $promise->reject("Did not find stopover at '$station' within trip '$train_id'"); + $promise->reject( +"Did not find stopover at '$station' within trip '$train_id'" + ); return; } @@ -640,7 +645,8 @@ sub startup { }; if ($@) { - $self->app->log->error("Checkin($uid): INSERT failed: $@"); + $self->app->log->error( + "Checkin($uid): INSERT failed: $@"); $promise->reject( 'INSERT failed: ' . $@ ); return; } @@ -652,21 +658,25 @@ sub startup { for my $coordinate ( $trip->polyline ) { if ( $coordinate->{stop} ) { if ( not defined $coordinate->{stop}->{eva} ) { - die() + die(); } push( @coordinate_list, [ - $coordinate->{lon}, $coordinate->{lat}, + $coordinate->{lon}, + $coordinate->{lat}, $coordinate->{stop}->{eva} ] ); - push( @station_list, $coordinate->{stop}->name ); + push( @station_list, + $coordinate->{stop}->name ); } else { - push( @coordinate_list, [ $coordinate->{lon}, $coordinate->{lat} ] ); + push( @coordinate_list, + [ $coordinate->{lon}, $coordinate->{lat} ] + ); } } @@ -680,9 +690,10 @@ sub startup { } else { $polyline = { - from_eva => ( $trip->stopovers )[0]->stop->{eva}, - to_eva => ( $trip->stopovers )[-1]->stop->{eva}, - coords => \@coordinate_list, + from_eva => + ( $trip->stopovers )[0]->stop->{eva}, + to_eva => ( $trip->stopovers )[-1]->stop->{eva}, + coords => \@coordinate_list, }; } } @@ -2267,7 +2278,7 @@ sub startup { my $ret = { deprecated => \0, checkedIn => ( - $status->{checked_in} + $status->{checked_in} or $status->{cancelled} ) ? \1 : \0, comment => $status->{comment}, @@ -2572,7 +2583,6 @@ sub startup { if ( $seen{$key} ) { next; } - $seen{$key} = 1; # direction does not matter at the moment @@ -2700,8 +2710,8 @@ sub startup { color => '#673ab7', opacity => @polylines ? $with_polyline - ? 0.4 - : 0.6 + ? 0.4 + : 0.6 : 0.8, }, { diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index 0e87b2a..e264c89 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -3270,7 +3270,8 @@ sub sync_backends_motis { } } - $db->update( 'schema_version', { motis => $Travel::Status::MOTIS::VERSION } ); + $db->update( 'schema_version', + { motis => $Travel::Status::MOTIS::VERSION } ); } sub setup_db { diff --git a/lib/Travelynx/Command/traewelling.pm b/lib/Travelynx/Command/traewelling.pm index ed40371..e4e0134 100644 --- a/lib/Travelynx/Command/traewelling.pm +++ b/lib/Travelynx/Command/traewelling.pm @@ -122,12 +122,12 @@ sub push_sync { my ($status) = @_; $push_result{ $status->{http} } += 1; } - )->catch( + )->catch( sub { my ($status) = @_; $push_result{ $status->{http} // 0 } += 1; } - )->wait; + )->wait; } return \%push_result; diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm index 4ff5c9e..9cc0ca1 100644 --- a/lib/Travelynx/Command/work.pm +++ b/lib/Travelynx/Command/work.pm @@ -197,10 +197,11 @@ sub run { for my $stopover ( $journey->stopovers ) { if ( not defined $stopover->stop->{eva} ) { - my $stop = $self->app->stations->get_by_external_id( + my $stop + = $self->app->stations->get_by_external_id( external_id => $stopover->stop->id, motis => $entry->{backend_name}, - ); + ); $stopover->stop->{eva} = $stop->{eva}; } @@ -220,7 +221,8 @@ sub run { } if ( not $found_departure ) { - $self->app->log->debug("Did not find $dep within trip $train_id"); + $self->app->log->debug( + "Did not find $dep within trip $train_id"); return; } @@ -235,7 +237,9 @@ sub run { ); } - if ( $found_arrival and $found_arrival->realtime_arrival ) { + if ( $found_arrival + and $found_arrival->realtime_arrival ) + { $self->app->in_transit->update_arrival_motis( uid => $uid, journey => $journey, diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 4c69f91..db0bc61 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1145,38 +1145,37 @@ sub backend_form { $backend->{longname} = $s->{name}; $backend->{homepage} = $s->{homepage}; $backend->{regions} = [ map { $place_map{$_} // $_ } - @{ $s->{coverage}{regions} // [] } ]; + @{ $s->{coverage}{regions} // [] } ]; $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; if ( $backend->{name} eq 'transitous' ) { - $backend->{regions} = [ 'Weltweit' ]; + $backend->{regions} = ['Weltweit']; } if ( $backend->{name} eq 'RNV' ) { $backend->{homepage} = 'https://rnv-online.de/'; } if ( - $s->{coverage}{area} + $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} // [] } ) + for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } ) { if ( $self->lonlat_in_polygon( $s_poly, [ $user_lon, $user_lat ] ) - ) + ) { push( @suggested_backends, $backend ); last; diff --git a/lib/Travelynx/Controller/Traewelling.pm b/lib/Travelynx/Controller/Traewelling.pm index 3cdeff8..6aa789c 100644 --- a/lib/Travelynx/Controller/Traewelling.pm +++ b/lib/Travelynx/Controller/Traewelling.pm @@ -29,7 +29,7 @@ sub oauth { redirect_uri => $self->base_url_for('/oauth/traewelling')->to_abs->scheme( $self->app->mode eq 'development' ? 'http' : 'https' - )->to_string, + )->to_string, scope => 'read-statuses write-statuses' } )->then( diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index aa7ee9b..cb78617 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -415,7 +415,7 @@ sub homepage { } else { @recent_targets = uniq_by { $_->{external_id_or_eva} } - $self->journeys->get_latest_checkout_stations( uid => $uid ); + $self->journeys->get_latest_checkout_stations( uid => $uid ); } $self->render( 'landingpage', @@ -667,8 +667,8 @@ sub geolocation { $self->render_later; Travel::Status::MOTIS->new_p( - promise => 'Mojo::Promise', - user_agent => $self->ua, + promise => 'Mojo::Promise', + user_agent => $self->ua, service => $motis_service, stops_by_coordinate => { @@ -1148,7 +1148,7 @@ sub station { timestamp => $timestamp, lookbehind => 30, lookahead => 30, - ) + ); } else { $promise = $self->iris->get_departures_p( @@ -1206,11 +1206,13 @@ sub station { elsif ($motis_service) { @results = map { $_->[0] } sort { $b->[1] <=> $a->[1] } - map { [ $_, $_->stopover->departure->epoch ] } $status->results; + map { [ $_, $_->stopover->departure->epoch ] } + $status->results; $status = { - station_eva => $station, - station_name => $status->{results}->[0]->stopover->stop->name, + station_eva => $station, + station_name => + $status->{results}->[0]->stopover->stop->name, related_stations => [], }; } @@ -2020,7 +2022,7 @@ sub journey_details { $delay = sprintf( 'mit %+d ', ( - $journey->{rt_arrival}->epoch + $journey->{rt_arrival}->epoch - $journey->{sched_arrival}->epoch ) / 60 ); diff --git a/lib/Travelynx/Helper/DBDB.pm b/lib/Travelynx/Helper/DBDB.pm index 79e9c0b..a310aa3 100644 --- a/lib/Travelynx/Helper/DBDB.pm +++ b/lib/Travelynx/Helper/DBDB.pm @@ -61,7 +61,8 @@ sub has_wagonorder_p { } } - $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} ) + $self->{user_agent}->request_timeout(5) + ->get_p( $url => $self->{header} ) ->then( sub { my ($tx) = @_; @@ -81,7 +82,7 @@ sub has_wagonorder_p { } return; } - )->catch( + )->catch( sub { my ($err) = @_; $self->{log}->debug("${debug_prefix}: n ($err)"); @@ -89,7 +90,7 @@ sub has_wagonorder_p { $promise->reject; return; } - )->wait; + )->wait; return $promise; } @@ -120,7 +121,8 @@ sub get_wagonorder_p { return $promise; } - $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} ) + $self->{user_agent}->request_timeout(5) + ->get_p( $url => $self->{header} ) ->then( sub { my ($tx) = @_; @@ -139,14 +141,14 @@ sub get_wagonorder_p { } return; } - )->catch( + )->catch( sub { my ($err) = @_; $self->{log}->debug("${debug_prefix}: error ${err}"); $promise->reject($err); return; } - )->wait; + )->wait; return $promise; } @@ -163,7 +165,8 @@ sub get_stationinfo_p { return $promise->resolve($content); } - $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} ) + $self->{user_agent}->request_timeout(5) + ->get_p( $url => $self->{header} ) ->then( sub { my ($tx) = @_; @@ -183,7 +186,7 @@ sub get_stationinfo_p { $promise->resolve($json); return; } - )->catch( + )->catch( sub { my ($err) = @_; $self->{log}->debug("get_stationinfo_p(${eva}): Error ${err}"); @@ -191,7 +194,7 @@ sub get_stationinfo_p { $promise->reject($err); return; } - )->wait; + )->wait; return $promise; } diff --git a/lib/Travelynx/Helper/MOTIS.pm b/lib/Travelynx/Helper/MOTIS.pm index ee2b10b..d4e1777 100644 --- a/lib/Travelynx/Helper/MOTIS.pm +++ b/lib/Travelynx/Helper/MOTIS.pm @@ -50,10 +50,10 @@ sub get_station_by_query_p { my $promise = Mojo::Promise->new; Travel::Status::MOTIS->new_p( - cache => $self->{cache}, - promise => 'Mojo::Promise', - user_agent => Mojo::UserAgent->new, - lwp_options => { + cache => $self->{cache}, + promise => 'Mojo::Promise', + user_agent => Mojo::UserAgent->new, + lwp_options => { timeout => 10, agent => $self->{header}{'User-Agent'}, }, @@ -106,10 +106,10 @@ sub get_departures_p { agent => $self->{header}{'User-Agent'}, }, - service => $opt{service}, - timestamp => $timestamp, - stop_id => $opt{station_id}, - results => 60, + service => $opt{service}, + timestamp => $timestamp, + stop_id => $opt{station_id}, + results => 60, ); } @@ -126,8 +126,8 @@ sub get_trip_p { promise => 'Mojo::Promise', user_agent => Mojo::UserAgent->new, - service => $opt{service}, - trip_id => $opt{trip_id}, + service => $opt{service}, + trip_id => $opt{trip_id}, )->then( sub { my ($motis) = @_; diff --git a/lib/Travelynx/Helper/Traewelling.pm b/lib/Travelynx/Helper/Traewelling.pm index 100a799..66f2a29 100644 --- a/lib/Travelynx/Helper/Traewelling.pm +++ b/lib/Travelynx/Helper/Traewelling.pm @@ -78,7 +78,8 @@ sub get_status_p { $self->{user_agent}->request_timeout(20) ->get_p( "https://traewelling.de/api/v1/user/${username}/statuses?limit=1" => - $header )->then( + $header ) + ->then( sub { my ($tx) = @_; if ( my $err = $tx->error ) { @@ -150,13 +151,13 @@ sub get_status_p { } } } - )->catch( + )->catch( sub { my ($err) = @_; $promise->reject( { text => "v1/${username}/statuses: $err" } ); return; } - )->wait; + )->wait; return $promise; } @@ -238,13 +239,13 @@ sub logout_p { return; } } - )->catch( + )->catch( sub { my ($err) = @_; $promise->reject("v1/auth/logout: $err"); return; } - )->wait; + )->wait; return $promise; } @@ -322,7 +323,8 @@ sub checkin_p { $self->{user_agent}->request_timeout(20) ->post_p( "https://traewelling.de/api/v1/trains/checkin" => $header => json => - $request )->then( + $request ) + ->then( sub { my ($tx) = @_; if ( my $err = $tx->error ) { @@ -368,7 +370,7 @@ sub checkin_p { # on the user status page return; } - )->catch( + )->catch( sub { my ($err) = @_; $self->{log}->debug("... $debug_prefix error: $err"); @@ -381,7 +383,7 @@ sub checkin_p { $promise->reject( { connection => $err } ); return; } - )->wait; + )->wait; return $promise; } diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm index 0e3fdc6..cc943b3 100644 --- a/lib/Travelynx/Model/InTransit.pm +++ b/lib/Travelynx/Model/InTransit.pm @@ -293,16 +293,20 @@ sub add { @route, [ $journey_stopover->stop->name, - $journey_stopover->stop->{eva} // die('eva not set for stopover'), + $journey_stopover->stop->{eva} + // die('eva not set for stopover'), { - sched_arr => _epoch( $journey_stopover->scheduled_arrival ), - sched_dep => _epoch( $journey_stopover->scheduled_departure ), - rt_arr => _epoch( $journey_stopover->realtime_arrival ), - rt_dep => _epoch( $journey_stopover->realtime_departure ), + sched_arr => + _epoch( $journey_stopover->scheduled_arrival ), + sched_dep => + _epoch( $journey_stopover->scheduled_departure ), + rt_arr => _epoch( $journey_stopover->realtime_arrival ), + rt_dep => + _epoch( $journey_stopover->realtime_departure ), arr_delay => $journey_stopover->arrival_delay, dep_delay => $journey_stopover->departure_delay, - lat => $journey_stopover->stop->lat, - lon => $journey_stopover->stop->lon, + lat => $journey_stopover->stop->lat, + lon => $journey_stopover->stop->lon, } ] ); @@ -384,7 +388,7 @@ sub postprocess { # Note that the departure stop may be present more than once in @route, # e.g. when traveling along ring lines such as S41 / S42 in Berlin. if ( - $ret->{dep_name} + $ret->{dep_name} and $station->[0] eq $ret->{dep_name} and not($station->[2]{sched_dep} and $station->[2]{sched_dep} < $ret->{sched_dep_ts} ) @@ -1149,16 +1153,18 @@ sub update_arrival_motis { @route, [ $journey_stopover->stop->name, - $journey_stopover->stop->{eva} // die('eva not set for stopover'), + $journey_stopover->stop->{eva} + // die('eva not set for stopover'), { sched_arr => _epoch( $journey_stopover->scheduled_arrival ), - sched_dep => _epoch( $journey_stopover->scheduled_departure ), - rt_arr => _epoch( $journey_stopover->realtime_arrival ), - rt_dep => _epoch( $journey_stopover->realtime_departure ), + sched_dep => + _epoch( $journey_stopover->scheduled_departure ), + rt_arr => _epoch( $journey_stopover->realtime_arrival ), + rt_dep => _epoch( $journey_stopover->realtime_departure ), arr_delay => $journey_stopover->arrival_delay, dep_delay => $journey_stopover->departure_delay, - lat => $journey_stopover->stop->lat, - lon => $journey_stopover->stop->lon, + lat => $journey_stopover->stop->lat, + lon => $journey_stopover->stop->lon, } ] ); diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index fff59f9..8efbab2 100755 --- a/lib/Travelynx/Model/Journeys.pm +++ b/lib/Travelynx/Model/Journeys.pm @@ -872,8 +872,10 @@ sub get_latest_checkout_stations { my $res = $db->select( 'journeys_str', [ - 'arr_name', 'arr_eva', 'arr_external_id', 'train_id', - 'backend_id', 'backend_name', 'is_dbris', 'is_hafas', + 'arr_name', 'arr_eva', + 'arr_external_id', 'train_id', + 'backend_id', 'backend_name', + 'is_dbris', 'is_hafas', 'is_motis' ], { @@ -898,11 +900,12 @@ sub get_latest_checkout_stations { { name => $row->{arr_name}, eva => $row->{arr_eva}, - external_id_or_eva => $row->{arr_external_id} // $row->{arr_eva}, - dbris => $row->{is_dbris} ? $row->{backend_name} : 0, - hafas => $row->{is_hafas} ? $row->{backend_name} : 0, - motis => $row->{is_motis} ? $row->{backend_name} : 0, - backend_id => $row->{backend_id}, + external_id_or_eva => $row->{arr_external_id} + // $row->{arr_eva}, + dbris => $row->{is_dbris} ? $row->{backend_name} : 0, + hafas => $row->{is_hafas} ? $row->{backend_name} : 0, + motis => $row->{is_motis} ? $row->{backend_name} : 0, + backend_id => $row->{backend_id}, } ); } @@ -1396,7 +1399,7 @@ sub compute_review { if ( not $most_undelay or $speedup > ( - $most_undelay->{sched_duration} + $most_undelay->{sched_duration} - $most_undelay->{rt_duration} ) ) @@ -1669,7 +1672,10 @@ sub compute_stats { @inconsistencies, { conflict => { - train => ( $journey->{is_motis} ? '' : $journey->{type} ) . ' ' + train => ( + $journey->{is_motis} ? '' : $journey->{type} + ) + . ' ' . ( $journey->{line} // $journey->{no} ), arr => epoch_to_dt( $journey->{rt_arr_ts} ) ->strftime('%d.%m.%Y %H:%M'), @@ -1695,7 +1701,8 @@ sub compute_stats { $next_departure = $journey->{rt_dep_ts}; $next_id = $journey->{id}; $next_train - = ( $journey->{is_motis} ? '' : $journey->{type} ) . ' ' . ( $journey->{line} // $journey->{no} ),; + = ( $journey->{is_motis} ? '' : $journey->{type} ) . ' ' + . ( $journey->{line} // $journey->{no} ),; } my $ret = { km_route => $km_route, @@ -1744,7 +1751,7 @@ sub get_stats { # checks out of a train or manually edits/adds a journey. if ( - not $opt{write_only} + not $opt{write_only} and not $opt{review} and my $stats = $self->stats_cache->get( uid => $uid, diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm index 761c5de..6758b93 100644 --- a/lib/Travelynx/Model/Stations.pm +++ b/lib/Travelynx/Model/Stations.pm @@ -99,8 +99,8 @@ sub get_backends { $opt{db} //= $self->{pg}->db; - my $res = $opt{db} - ->select( 'backends', [ 'id', 'name', 'iris', 'hafas', 'dbris', 'motis' ] ); + my $res = $opt{db}->select( 'backends', + [ 'id', 'name', 'iris', 'hafas', 'dbris', 'motis' ] ); my @ret; while ( my $row = $res->hash ) { @@ -206,12 +206,8 @@ sub add_or_update { returning * }, ( - $opt{backend_id}, - $stop->id, - $stop->name, - $stop->lat, - $stop->lon, - 0, + $opt{backend_id}, $stop->id, $stop->name, + $stop->lat, $stop->lon, 0, ) ); diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 10ab17e..1c3692e 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -1027,11 +1027,11 @@ sub get_followers { id => $row->{id}, name => $row->{name}, following_back => ( - $row->{inverse_predicate} + $row->{inverse_predicate} and $row->{inverse_predicate} == $predicate_atoi{follows} ) ? 1 : 0, followback_requested => ( - $row->{inverse_predicate} + $row->{inverse_predicate} and $row->{inverse_predicate} == $predicate_atoi{requests_follow} ) ? 1 : 0, @@ -1103,7 +1103,7 @@ sub get_followees { id => $row->{id}, name => $row->{name}, following_back => ( - $row->{inverse_predicate} + $row->{inverse_predicate} and $row->{inverse_predicate} == $predicate_atoi{follows} ) ? 1 : 0, } -- cgit v1.2.3 From 9c52614dbdb4ea95ad06a4d2fa2d611fb6248501 Mon Sep 17 00:00:00 2001 From: Birte Kristina Friesel Date: Mon, 9 Jun 2025 15:30:59 +0200 Subject: backend selection: group recommended / transit assoc / experimental&legacy --- lib/Travelynx/Controller/Account.pm | 18 ++++++++++---- templates/select_backend.html.ep | 47 ++++++++++++++++++++++++++++++------- 2 files changed, 53 insertions(+), 12 deletions(-) (limited to 'lib/Travelynx/Controller/Account.pm') diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index db0bc61..033b270 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -1069,11 +1069,13 @@ sub backend_form { $backend->{name} = 'IRIS'; $backend->{longname} = 'Deutsche Bahn: IRIS-TTS'; $backend->{homepage} = 'https://www.bahn.de'; + $backend->{legacy} = 1; } elsif ( $backend->{dbris} ) { - $type = 'DBRIS'; - $backend->{longname} = 'Deutsche Bahn: bahn.de'; - $backend->{homepage} = 'https://www.bahn.de'; + $type = 'DBRIS'; + $backend->{longname} = 'Deutsche Bahn: bahn.de'; + $backend->{homepage} = 'https://www.bahn.de'; + $backend->{recommended} = 1; } elsif ( $backend->{hafas} ) { @@ -1105,6 +1107,13 @@ sub backend_form { @{ $s->{coverage}{regions} // [] } ]; $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + if ( $backend->{name} eq 'ÖBB' ) { + $backend->{recommended} = 1; + } + else { + $backend->{association} = 1; + } + if ( $s->{coverage}{area} and $s->{coverage}{area}{type} eq 'Polygon' @@ -1146,7 +1155,8 @@ sub backend_form { $backend->{homepage} = $s->{homepage}; $backend->{regions} = [ map { $place_map{$_} // $_ } @{ $s->{coverage}{regions} // [] } ]; - $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + $backend->{experimental} = 1; if ( $backend->{name} eq 'transitous' ) { $backend->{regions} = ['Weltweit']; diff --git a/templates/select_backend.html.ep b/templates/select_backend.html.ep index 19eaee5..a65b1aa 100644 --- a/templates/select_backend.html.ep +++ b/templates/select_backend.html.ep @@ -3,7 +3,7 @@

    Backend auswählen

    Das ausgewählte Backend bestimmt die Datenquelle für Fahrten in travelynx. - Hilfe bei der Auswahl. + Details.

    @@ -15,6 +15,11 @@

    Vorschläge

    +

    + Anhand der Zielstation der letzten Fahrt und den + empfohlenen Nutzungsbereichen der verfügbaren Backends + (soweit bekannt). +

    % for my $backend (@{ stash('suggestions') // [] }) { @@ -23,10 +28,39 @@ % }
    -

    Alle Backends

    +

    Empfohlen

    +

    + bahn.de für Regional- und Fernverkehr in Deutschland. + ÖBB für Nah-, Regional- und Fernverkehr in Österreich sowie Regional- und Fernverkehr in der EU. +

    - % for my $backend (@{ stash('backends') // [] }) { + % for my $backend (grep { $_->{recommended} } @{ stash('backends') // [] }) { + %= include '_backend_line', user => $user, backend => $backend + % } +
    +
    +

    Verbünde

    +

    + Diese Backends sind meist die beste Wahl für für + Nahverkehrsfahrten in der jeweiligen Region. + Backends außerhalb Deutschlands sind im Regelfall auch + für dortigen Regional- und Fernverkehr die beste Wahl. +

    +
    +
    + % for my $backend (grep { $_->{association} } @{ stash('backends') // [] }) { + %= include '_backend_line', user => $user, backend => $backend + % } +
    +
    +

    Experimentell oder abgekündigt

    +

    + Einchecken auf eigene Gefahr. +

    +
    +
    + % for my $backend (grep { $_->{experimental} or $_->{legacy} } @{ stash('backends') // [] }) { %= include '_backend_line', user => $user, backend => $backend % } %= end @@ -35,12 +69,9 @@

    Hilfe

    Deutsche Bahn: bahn.de 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. - Wagenreihungen sind nur bei Fahrten des Fernverkehrs sowie Zügen ohne Liniennummer verfügbar. - Verspätungsmeldungen werden aktuell nicht berücksichtigt. - bahn.de ist das einzige Backend, welches Synchronisierung mit Träwelling unterstützt. + Die Implementierung ist noch recht frisch, bietet jedoch prinzipiell akkurate Echtzeit- und Kartendaten sowie Wagenreihungen.

    - Deutsche Bahn: IRIS-TTS liefert Echtzeitdaten (nur am Start- und Zielbahnhof), Wagenreihungen und Verspätungsmeldungen für Regional- und Fernverkehr in Deutschland. In vielen Fällen sind auch Kartendaten verfügbar. + Deutsche Bahn: IRIS-TTS liefert Echtzeitdaten (nur am Start- und Zielbahnhof), Wagenreihungen und Verspätungsmeldungen für Regional- und Fernverkehr in Deutschland. Kartendaten sind nur teilweise verfügbar. ÖBB liefern Kartendaten und Wagenreihungen für Fernverkehr in Deutschland und Umgebung, jedoch keine Meldungen. Echtzeitdaten sind teilweise verfügbar.

    -- cgit v1.2.3