summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cpanfile1
-rwxr-xr-xlib/Travelynx.pm170
-rw-r--r--lib/Travelynx/Command/database.pm214
-rw-r--r--lib/Travelynx/Command/work.pm95
-rw-r--r--lib/Travelynx/Controller/Account.pm47
-rwxr-xr-xlib/Travelynx/Controller/Api.pm2
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm136
-rw-r--r--lib/Travelynx/Helper/MOTIS.pm158
-rw-r--r--lib/Travelynx/Model/InTransit.pm131
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm28
-rw-r--r--lib/Travelynx/Model/Stations.pm96
-rw-r--r--lib/Travelynx/Model/Users.pm9
-rw-r--r--public/static/js/geolocation.js22
-rw-r--r--public/static/js/travelynx-actions.js24
-rw-r--r--sass/src/common/local.scss18
-rw-r--r--templates/_backend_line.html.ep2
-rw-r--r--templates/_departures_motis.html.ep54
-rw-r--r--templates/_format_train.html.ep4
-rw-r--r--templates/_history_trains.html.ep5
-rw-r--r--templates/changelog.html.ep14
-rw-r--r--templates/departures.html.ep18
-rw-r--r--templates/landingpage.html.ep2
22 files changed, 1191 insertions, 59 deletions
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 <git@nwex.de>
#
# 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;
@@ -260,6 +262,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) = @_;
state $trwl = Travelynx::Model::Traewelling->new( pg => $self->pg );
@@ -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);
}
@@ -557,6 +574,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 <git@nwex.de>
#
# 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 <git@nwex.de>
#
# 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 <git@nwex.de>
#
# 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 <git@nwex.de>
#
# 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 <git@nwex.de>
+#
+# 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 <git@nwex.de>
#
# 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 <git@nwex.de>
#
# 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 = $('<a class="tablerow" href="/s/' + parts[0] + '?dbris=' + parts[2] + '&amp;hafas=' + parts[3] + '"><span><i class="material-icons" aria-hidden="true">' + (parts[2] == '0' ? 'train' : 'directions') + '</i>' + parts[1] + '</span></a>');
+ const [ eva, name, dbris, motis, hafas ] = parts;
+
+ const node = $('<a class="tablerow" href="/s/' + eva + '?dbris=' + dbris + '&amp;motis=' + motis + '&amp;hafas=' + hafas + '"><span><i class="material-icons" aria-hidden="true">' + (dbris == '0' ? 'train' : 'directions') + '</i>' + name + '</span></a>');
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 = $('<a class="tablerow" href="/s/' + id + '?motis=' + motis + '"><span><i class="material-icons" aria-hidden="true">train</i>' + name + '</span></a>');
+ } 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 = $('<a class="tablerow" href="/s/' + eva + '?hafas=' + hafas + '"><span><i class="material-icons" aria-hidden="true">' + (hafas == '0' ? 'train' : 'directions') + '</i>' + name + '</span></a>');
+ }
- const node = $('<a class="tablerow" href="/s/' + eva + '?hafas=' + hafas + '"><span><i class="material-icons" aria-hidden="true">' + (hafas == '0' ? 'train' : 'directions') + '</i>' + name + '</span></a>');
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}) {
<br/>
- <a href="https://dbf.finalrewind.org/coverage/HAFAS/<%= $backend->{name} %>"><%= join(q{, }, @{$backend->{regions} // []}) || '[Karte]' %></a>
+ <a href="https://dbf.finalrewind.org/coverage/<%= $backend->{type} %>/<%= $backend->{name} %>"><%= join(q{, }, @{$backend->{regions} // []}) || '[Karte]' %></a>
% }
% elsif ($backend->{regions}) {
<br/>
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 @@
+<ul class="collection departures">
+% my $orientation_bar_shown = param('train');
+% my $now_epoch = now->epoch;
+% for my $result (@{$results}) {
+ % my $row_class = '';
+ % my $link_class = 'action-checkin';
+ % if ($result->is_cancelled) {
+ % $row_class = "cancelled";
+ % $link_class = 'action-cancelled-from';
+ % }
+ % if (not $orientation_bar_shown and $result->stopover->departure->epoch < $now_epoch) {
+ % $orientation_bar_shown = 1;
+ <li class="collection-item" id="now">
+ <strong class="dep-time">
+ %= now->strftime('%H:%M')
+ </strong>
+ <strong>— Anfragezeitpunkt —</strong>
+ </li>
+ % }
+ <li class="collection-item <%= $link_class %> <%= $row_class %>"
+ data-motis="<%= $motis %>"
+ data-station="<%= $result->stopover->stop->id %>"
+ data-train="<%= $result->id %>"
+ data-ts="<%= ($result->stopover->departure)->epoch %>"
+ >
+ <a class="dep-time" href="#">
+ %= $result->stopover->departure->strftime('%H:%M')
+ % if ($result->stopover->delay) {
+ (<%= sprintf('%+d', $result->stopover->delay) %>)
+ % }
+ % elsif (not $result->stopover->is_realtime and not $result->stopover->is_cancelled) {
+ <i class="material-icons" aria-label="Keine Echtzeitdaten vorhanden" style="font-size: 16px;">gps_off</i>
+ % }
+ </a>
+ <span class="dep-line <%= $result->mode %>" style="background-color: #<%= $result->route_color // q{} %>;">
+ %= $result->route_name
+ </span>
+ <span class="dep-dest">
+ % if ($result->is_cancelled) {
+ Fahrt nach <%= $result->headsign %> entfällt
+ % }
+ % else {
+ %= $result->headsign
+ % for my $checkin (@{$checkin_by_train->{$result->id} // []}) {
+ <span class="followee-checkin">
+ <i class="material-icons tiny" aria-label="Eine Person, der du folgst, ist hier eingecheckt">people</i>
+ <%= $checkin->{followee_name} %> → <%= $checkin->{arr_name} // '???' %>
+ </span>
+ % }
+ % }
+ </span>
+ </li>
+% }
+</ul>
diff --git a/templates/_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 @@
🏳️‍🌈
% }
<span class="dep-line <%= $journey->{train_type} // q{} %>">
- <%= $journey->{train_type} %>
+ % if (not $journey->{is_motis}) {
+ <%= $journey->{train_type} %>
+ % }
<%= $journey->{train_line} // $journey->{train_no}%>
</span>
% 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 @@
<li class="collection-item">
<a href="<%= $detail_link %>">
<span class="dep-line <%= $travel->{type} // q{} %>">
- <%= $travel->{type} %> <%= $travel->{line} // $travel->{no}%>
+ % if (not $travel->{is_motis}) {
+ <%= $travel->{type} %>
+ % }
+ <%= $travel->{line} // $travel->{no}%>
</span>
</a>
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
@@ -2,6 +2,20 @@
<div class="row">
<div class="col s12 m1 l1">
+ 2.13
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Experimentelle Unterstützung für Checkins via MOTIS-Backends
+ (derzeit transitous und RNV). Vielen Dank an <a href="https://github.com/networkException">networkException</a>
+ für die Implementierung der API und Einbindung in travelynx.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
2.12
</div>
<div class="col s12 m11 l11">
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')) {
<a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= param('hafas') %></a>
% }
+ % elsif (param('motis')) {
+ <a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= param('motis') %></a>
+ % }
% else {
% if ($user->{backend_id}) {
<a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= $user->{backend_name} %></a>
@@ -33,7 +36,13 @@
<div class="card">
<div class="card-content">
<span class="card-title">Aktuell eingecheckt</span>
- <p>In <%= $user_status->{train_type} %> <%= $user_status->{train_no} %>
+ <p>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 @@
<div class="row">
<div class="col s4 center-align">
- % if ($dbris or $hafas) {
+ % if ($dbris or $hafas or $motis) {
<a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({dbris => $dbris, hafas => $hafas, timestamp => $datetime->clone->subtract(hours => 1)->epoch}) %>"><i class="material-icons left" aria-hidden="true">chevron_left</i><span class="hide-on-small-only">früher</span></a>
% }
</div>
@@ -106,7 +115,7 @@
% }
</div>
<div class="col s4 center-align">
- % if ($dbris or $hafas) {
+ % if ($dbris or $hafas or $motis) {
<a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({dbris => $dbris, hafas => $hafas, timestamp => $datetime->clone->add(hours => 1)->epoch}) %>"><span class="hide-on-small-only">später</span><i class="material-icons right" aria-hidden="true">chevron_right</i></a>
% }
</div>
@@ -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 @@
<div class="card-content">
<span class="card-title">Hallo, <%= $user->{name} %>!</span>
<p>Du bist gerade nicht eingecheckt.</p>
- <div class="geolocation" data-recent="<%= join('|', map { $_->{eva} . ';' . $_->{name} . ';' . $_->{dbris} . ';' . $_->{hafas} } @{stash('recent_targets') // []} ) %>" data-backend="<%= $user->{backend_id} %>">
+ <div class="geolocation" data-recent="<%= join('|', map { $_->{external_id_or_eva} . ';' . $_->{name} . ';' . $_->{dbris} . ';' . $_->{motis} . ';' . $_->{hafas} } @{stash('recent_targets') // []} ) %>" data-backend="<%= $user->{backend_id} %>">
<a class="btn waves-effect waves-light btn-flat request">Stationen in der Umgebung abfragen</a>
</div>
%= hidden_field backend_dbris => $user->{backend_dbris}