summaryrefslogtreecommitdiff
path: root/lib/Travelynx
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx')
-rw-r--r--lib/Travelynx/Command/database.pm440
-rw-r--r--lib/Travelynx/Command/dumpstops.pm6
-rw-r--r--lib/Travelynx/Command/integritycheck.pm9
-rw-r--r--lib/Travelynx/Command/maintenance.pm2
-rw-r--r--lib/Travelynx/Command/stats.pm59
-rw-r--r--lib/Travelynx/Command/translation.pm99
-rw-r--r--lib/Travelynx/Command/work.pm365
-rw-r--r--lib/Travelynx/Controller/Account.pm57
-rwxr-xr-xlib/Travelynx/Controller/Api.pm14
-rwxr-xr-xlib/Travelynx/Controller/Profile.pm9
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm816
-rw-r--r--lib/Travelynx/Helper/DBDB.pm201
-rw-r--r--lib/Travelynx/Helper/DBRIS.pm105
-rw-r--r--lib/Travelynx/Helper/Locales.pm28
-rw-r--r--lib/Travelynx/Helper/MOTIS.pm3
-rw-r--r--lib/Travelynx/Model/InTransit.pm337
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm355
-rw-r--r--lib/Travelynx/Model/Stations.pm37
-rw-r--r--lib/Travelynx/Model/Users.pm19
19 files changed, 2341 insertions, 620 deletions
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm
index 675f0a7..5792e5f 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -7,7 +7,7 @@ package Travelynx::Command::database;
use Mojo::Base 'Mojolicious::Command';
use DateTime;
-use File::Slurp qw(read_file);
+use File::Slurp qw(read_dir read_file);
use List::Util qw();
use JSON;
use Travel::Status::DE::EFA;
@@ -3184,8 +3184,446 @@ qq{select distinct checkout_station_id from in_transit where backend_id = 0;}
}
);
},
+
+ # v64 -> v65
+ # stations_str: add is_motis
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view stations_str;
+ create view stations_str as
+ select stations.name as name,
+ eva, lat, lon,
+ backends.name as backend,
+ dbris as is_dbris,
+ efa as is_efa,
+ iris as is_iris,
+ hafas as is_hafas,
+ motis as is_motis
+ from stations
+ left join backends
+ on source = backends.id;
+ update schema_version set version = 65;
+ }
+ );
+ },
+
+ # v65 -> v66
+ # Relax platform and line length constraints for EFA APIs (and possibly MOTIS)
+ 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 in_transit alter column train_line type varchar(64);
+ alter table in_transit alter column arr_platform type varchar(64);
+ alter table in_transit alter column dep_platform type varchar(64);
+ alter table journeys alter column train_line type varchar(64);
+ alter table journeys alter column arr_platform type varchar(64);
+ alter table journeys alter column dep_platform type varchar(64);
+
+ 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
+ ;
+
+ update schema_version set version = 66;
+ }
+ );
+ },
+
+ # v66 -> v67
+ # Add language settings to profile
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view users_with_backend;
+ alter table users add column language varchar(128);
+ update schema_version set version = 67;
+ create view users_with_backend as select
+ users.id as id, users.name as name, status, public_level,
+ language, 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
+ ;
+ }
+ );
+ },
+
+ # v67 -> v68
+ # Of course there are backends with stop names that are >64 chars long
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view stations_str;
+ drop view stations_with_external_ids;
+ drop view in_transit_str;
+ drop view journeys_str;
+ drop view follows_in_transit;
+ alter table stations alter column name type varchar(128);
+ create view stations_str as
+ select stations.name as name,
+ eva, lat, lon,
+ backends.name as backend,
+ dbris as is_dbris,
+ efa as is_efa,
+ iris as is_iris,
+ hafas as is_hafas,
+ motis as is_motis
+ from stations
+ left join backends
+ on source = backends.id;
+ 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
+ ;
+ 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 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
+ ;
+ update schema_version set version = 68;
+ }
+ );
+ },
+
+ # v68 -> v69
+ # Incorporate dbdb (entry/exit direction) data into travelynx
+ # This avoids having to make web requests to lib.finalrewind.org/dbdb,
+ # and allows for also showing the exit direction for intermediate stops.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table schema_version
+ add column dbdb varchar(12);
+ create table bahn_platform_directions (
+ eva integer primary key,
+ data jsonb not null
+ );
+ }
+ );
+ sync_dbdb($db);
+ $db->query(
+ qq{
+ update schema_version set version = 69;
+ update schema_version set dbdb = '2025-10-27';
+ }
+ );
+ },
);
+sub sync_dbdb {
+ my ($db) = @_;
+
+ my $json = JSON->new;
+
+ for my $file ( read_dir( 'ext/dbdb/s', prefix => 1 ) ) {
+ if ( $file !~ m{\.txt$} ) {
+ next;
+ }
+
+ my %station;
+ for my $line ( read_file( $file, { binmode => ':encoding(utf-8)' } ) ) {
+ if ( $line
+ =~ m{ ^ \s* (?<platform> \d+ ) \s+ (?<type> \S+ ) \s+ (?<direction> \S+ ) }x
+ )
+ {
+ $station{ $+{platform} } = {
+ kopfgleis => $+{type} eq 'K' ? \1 : \0,
+ direction => $+{direction},
+ };
+ }
+ elsif ( $line
+ =~ m{ ^ @ \s* (?<stations> [^:]+ ) : \s* (?<platforms> .+ ) $ }x
+ )
+ {
+ my $stations_raw = $+{stations};
+ my $platforms_raw = $+{platforms};
+ my @stations = split( qr{, }, $stations_raw );
+ my @platforms = split( qr{, }, $platforms_raw );
+ for my $platform (@platforms) {
+ my ( $number, $direction ) = split( qr{ }, $platform );
+ for my $from_station (@stations) {
+ $station{$number}{direction_from}{$from_station}
+ = $direction;
+ }
+ }
+ }
+ }
+ my ($station_name) = ( $file =~ m{ s / ([^.]*) . txt $ }x );
+ my ($station)
+ = Travel::Status::DE::IRIS::Stations::get_station($station_name);
+ if ( $station and $station->[0] eq $station_name ) {
+ $db->insert(
+ 'bahn_platform_directions',
+ {
+ eva => $station->[2],
+ data => $json->encode( \%station )
+ },
+ { on_conflict => \'(eva) do update set data = EXCLUDED.data' }
+ );
+ }
+ elsif ( not $station ) {
+ say STDERR "DBDB import: unknown station: $station_name";
+ }
+ else {
+ say STDERR
+"DBDB import: station mismatch: wanted to import $station_name, but got "
+ . $station->[0];
+ }
+ }
+}
+
sub sync_stations {
my ( $db, $iris_version ) = @_;
diff --git a/lib/Travelynx/Command/dumpstops.pm b/lib/Travelynx/Command/dumpstops.pm
index 4d20bbd..15f5861 100644
--- a/lib/Travelynx/Command/dumpstops.pm
+++ b/lib/Travelynx/Command/dumpstops.pm
@@ -1,6 +1,6 @@
package Travelynx::Command::dumpstops;
-# Copyright (C) 2024 Birte Kristina Friesel
+# Copyright (C) 2024-2025 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -24,13 +24,13 @@ sub run {
or die("open($filename): $!\n");
my $csv = Text::CSV->new( { eol => "\r\n" } );
- $csv->combine(qw(name eva lat lon backend is_iris is_hafas));
+ $csv->combine(qw(name eva lat lon backend is_dbris is_efa is_iris is_hafas is_motis));
print $fh $csv->string;
my $iter = $self->app->stations->get_db_iterator;
while ( my $row = $iter->hash ) {
$csv->combine(
- @{$row}{qw{name eva lat lon backend is_iris is_hafas}} );
+ @{$row}{qw{name eva lat lon backend is_dbris is_efa is_iris is_hafas is_motis}} );
print $fh $csv->string;
}
close($fh);
diff --git a/lib/Travelynx/Command/integritycheck.pm b/lib/Travelynx/Command/integritycheck.pm
index be5fe71..907d484 100644
--- a/lib/Travelynx/Command/integritycheck.pm
+++ b/lib/Travelynx/Command/integritycheck.pm
@@ -76,7 +76,8 @@ sub run {
my %notified;
my $rename = $self->app->renamed_station;
- my $res = $db->select( 'journeys', [ 'route', 'edited' ] )->expand;
+ my $res = $db->select( 'journeys', [ 'backend_id', 'route', 'edited' ] )
+ ->expand;
while ( my $j = $res->hash ) {
if ( $j->{edited} & 0x0010 ) {
@@ -89,8 +90,10 @@ sub run {
$stop->[0] = $rename->{ $stop->[0] };
}
}
- my @unknown
- = $self->app->stations->grep_unknown( map { $_->[0] } @stops );
+ my @unknown = $self->app->stations->grep_unknown(
+ backend_id => $j->{backend_id},
+ names => [ map { $_->[0] } @stops ]
+ );
for my $stop_name (@unknown) {
if ( not $notified{$stop_name} ) {
if ( not $found ) {
diff --git a/lib/Travelynx/Command/maintenance.pm b/lib/Travelynx/Command/maintenance.pm
index 7baf762..7a8ae16 100644
--- a/lib/Travelynx/Command/maintenance.pm
+++ b/lib/Travelynx/Command/maintenance.pm
@@ -121,7 +121,7 @@ sub run {
push( @uids_to_delete,
$to_delete->arrays->map( sub { shift->[0] } )->each );
- if ( @uids_to_delete > 10 ) {
+ if ( @uids_to_delete > 60 ) {
printf STDERR (
"About to delete %d accounts, which is quite a lot.\n",
scalar @uids_to_delete
diff --git a/lib/Travelynx/Command/stats.pm b/lib/Travelynx/Command/stats.pm
new file mode 100644
index 0000000..953c75d
--- /dev/null
+++ b/lib/Travelynx/Command/stats.pm
@@ -0,0 +1,59 @@
+package Travelynx::Command::stats;
+
+# Copyright (C) 2020-2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+use Mojo::Base 'Mojolicious::Command';
+
+use DateTime;
+
+has description => 'Deal with monthly and yearly statistics';
+
+has usage => sub { shift->extract_usage };
+
+sub refresh_all {
+ my ($self) = @_;
+
+ my $db = $self->app->pg->db;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ say 'Refreshing all stats, this may take a while ...';
+
+ my $total = $db->select( 'users', 'count(*) as count', { status => 1 } )
+ ->hash->{count};
+ my $i = 1;
+
+ for
+ my $user ( $db->select( 'users', ['id'], { status => 1 } )->hashes->each )
+ {
+ $self->app->journeys->generate_missing_stats( uid => $user->{id} );
+ $self->app->journeys->get_stats(
+ uid => $user->{id},
+ year => $now->year,
+ write_only => 1,
+ );
+ if ( $i == $total or ( $i % 10 ) == 0 ) {
+ printf( "%.f%% complete\n", $i * 100 / $total );
+ }
+ $i++;
+ }
+}
+
+sub run {
+ my ( $self, $cmd, @arg ) = @_;
+
+ if ( $cmd eq 'refresh-all' ) {
+ $self->refresh_all(@arg);
+ }
+
+}
+
+1;
+
+__END__
+
+=head1 SYNOPSIS
+
+ Usage: index.pl stats refresh-all
+
+ Refreshes all stats
diff --git a/lib/Travelynx/Command/translation.pm b/lib/Travelynx/Command/translation.pm
new file mode 100644
index 0000000..cc3a5ac
--- /dev/null
+++ b/lib/Travelynx/Command/translation.pm
@@ -0,0 +1,99 @@
+package Travelynx::Command::translation;
+
+# Copyright (C) 2025 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use Mojo::Base 'Mojolicious::Command';
+use Travelynx::Helper::Locales;
+
+has description => 'Export translation status';
+
+has usage => sub { shift->extract_usage };
+
+sub run {
+ my ( $self, $command ) = @_;
+
+ my @locales = (qw(de-DE en-GB fr-FR hu-HU pl-PL));
+
+ my %count;
+ my %handle;
+ for my $locale (@locales) {
+ $handle{$locale} = Travelynx::Helper::Locales->get_handle($locale);
+ $handle{$locale}->fail_with('failure_handler_auto');
+ $count{$locale} = 0;
+ }
+
+ binmode( STDOUT, ':encoding(utf-8)' );
+
+ if ( not $command ) {
+ $self->help;
+ }
+ elsif ( $command eq 'update-ref' ) {
+ my @buf;
+
+ open( my $fh, '<:encoding(utf-8)', 'share/locales/de_DE.po' );
+ my $comment;
+ for my $line (<$fh>) {
+ chomp $line;
+ if ( $line =~ m{ ^ [#] \s+ (.*) $ }x ) {
+ push( @buf, "## $1\n" );
+ }
+ elsif ( $line =~ m{ ^ [#] , \s+ (.*) $ }x ) {
+ $comment = $1;
+ }
+ elsif ( $line =~ m{ ^ msgid \s+ " (.*) " $ }x ) {
+ my $id = $1;
+ push( @buf, "### ${id}\n" );
+ if ($comment) {
+ push( @buf, '*' . $comment . "*\n" );
+ $comment = undef;
+ }
+ for my $locale (@locales) {
+ my $translation = $handle{$locale}->maketext($id);
+ if ( $translation ne $id ) {
+ push( @buf, "* ${locale}: ${translation}" );
+ $count{$locale} += 1;
+ }
+ else {
+ push( @buf, "* ${locale} *missing*" );
+ }
+ }
+ push( @buf, q{} );
+ }
+ }
+ close($fh);
+
+ open( $fh, '>:encoding(utf-8)', 'share/locales/reference.md' );
+ say $fh '# Translation Status';
+ say $fh q{};
+ for my $locale (@locales) {
+ say $fh sprintf(
+ '* %s: %.1f%% complete (%d missing)',
+ $locale,
+ $count{$locale} * 100 / $count{'de-DE'},
+ $count{'de-DE'} - $count{$locale},
+ );
+ }
+ say $fh q{};
+ for my $line (@buf) {
+ say $fh $line;
+ }
+ close($fh);
+ }
+ else {
+ $self->help;
+ }
+}
+
+1;
+
+__END__
+
+=head1 SYNOPSIS
+
+ Usage: index.pl translation <command>
+
+ Supported commands:
+
+ * update-ref: update share/locales/reference.md
diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm
index 60417b1..dc58a48 100644
--- a/lib/Travelynx/Command/work.pm
+++ b/lib/Travelynx/Command/work.pm
@@ -18,7 +18,7 @@ has description => 'Update real-time data of active journeys';
has usage => sub { shift->extract_usage };
sub run {
- my ($self) = @_;
+ my ( $self, $backend ) = @_;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
my $checkin_deadline = $now->clone->subtract( hours => 48 );
@@ -53,16 +53,34 @@ sub run {
my $arr = $entry->{arr_eva};
my $train_id = $entry->{train_id};
- if ( $entry->{is_dbris} ) {
+ if ( $train_id eq 'manual' ) {
+ if ( $arr
+ and $entry->{real_arr_ts}
+ and $now->epoch - $entry->{real_arr_ts} > 900 )
+ {
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ }
+
+ elsif ( $entry->{is_dbris} and ( not $backend or $backend eq 'dbris' ) )
+ {
eval {
- Mojo::Promise->timer( $dbris_rate_limited ? 4.5 : 1.0 )->then(
+ Mojo::Promise->timer(
+ $dbris_rate_limited ? 4.5 : ( $backend ? 2.0 : 1.0 ) )
+ ->then(
sub {
return $self->app->dbris->get_journey_p(
trip_id => $train_id );
}
- )->then(
+ )->then(
sub {
my ($journey) = @_;
@@ -155,7 +173,7 @@ sub run {
)->wait;
}
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$self->app->log->debug(
@@ -169,11 +187,11 @@ sub run {
$backend_issues += 1;
}
}
- )->wait;
+ )->wait;
if ( $arr
and $entry->{real_arr_ts}
- and $now->epoch - $entry->{real_arr_ts} > 600 )
+ and $now->epoch - $entry->{real_arr_ts} > 900 )
{
$self->app->checkout_p(
station => $arr,
@@ -189,10 +207,9 @@ sub run {
$self->app->log->error(
"work($uid) @ DBRIS $entry->{backend_name}: $@");
}
- next;
}
- if ( $entry->{is_efa} ) {
+ elsif ( $entry->{is_efa} and ( not $backend or $backend eq 'efa' ) ) {
eval {
$self->app->efa->get_journey_p(
trip_id => $train_id,
@@ -269,7 +286,7 @@ sub run {
if ( $arr
and $entry->{real_arr_ts}
- and $now->epoch - $entry->{real_arr_ts} > 600 )
+ and $now->epoch - $entry->{real_arr_ts} > 900 )
{
$self->app->checkout_p(
station => $arr,
@@ -285,10 +302,10 @@ sub run {
$self->app->log->error(
"work($uid) @ EFA $entry->{backend_name}: $@");
}
- next;
}
- if ( $entry->{is_motis} ) {
+ elsif ( $entry->{is_motis} and ( not $backend or $backend eq 'motis' ) )
+ {
eval {
$self->app->motis->get_trip_p(
@@ -309,6 +326,10 @@ sub run {
stop => $stopover->stop,
motis => $entry->{backend_name},
);
+
+ $self->app->log->debug( "mapped "
+ . $stopover->stop->id . " to "
+ . $stopover->stop->{eva} );
}
}
@@ -358,7 +379,7 @@ sub run {
)->catch(
sub {
my ($err) = @_;
- $self->app->log->error(
+ $self->app->log->debug(
"work($uid) @ MOTIS $entry->{backend_name}: journey: $err"
);
}
@@ -366,7 +387,7 @@ sub run {
if ( $arr
and $entry->{real_arr_ts}
- and $now->epoch - $entry->{real_arr_ts} > 600 )
+ and $now->epoch - $entry->{real_arr_ts} > 900 )
{
$self->app->checkout_p(
station => $arr,
@@ -382,10 +403,10 @@ sub run {
$self->app->log->error(
"work($uid) @ MOTIS $entry->{backend_name}: $@");
}
- next;
}
- if ( $entry->{is_hafas} ) {
+ elsif ( $entry->{is_hafas} and ( not $backend or $backend eq 'hafas' ) )
+ {
eval {
@@ -437,8 +458,9 @@ sub run {
is_departure => 1,
eva => $dep,
datetime => $found_dep->sched_dep,
- train_type => $journey->type =~ s{ +$}{}r,
- train_no => $journey->number,
+ train_type => ( $journey->type // q{} )
+ =~ s{ +$}{}r,
+ train_no => $journey->number,
);
$self->app->add_stationinfo( $uid, 1,
$journey->id, $found_dep->loc->eva );
@@ -500,7 +522,7 @@ sub run {
if ( $arr
and $entry->{real_arr_ts}
- and $now->epoch - $entry->{real_arr_ts} > 600 )
+ and $now->epoch - $entry->{real_arr_ts} > 900 )
{
$self->app->checkout_p(
station => $arr,
@@ -516,7 +538,6 @@ sub run {
$self->app->log->error(
"work($uid) @ HAFAS $entry->{backend_name}: $@");
}
- next;
}
# TODO irgendwo ist hier ne race condition wo ein neuer checkin (in HAFAS) mit IRIS-Daten überschrieben wird.
@@ -528,182 +549,186 @@ sub run {
# update departure data for up to 15 minutes after departure and
# delaying automatic checkout by at least 10 minutes.
- eval {
- if ( $now->epoch - $entry->{real_dep_ts} < 900 ) {
- my $status = $self->app->iris->get_departures(
- station => $dep,
- lookbehind => 30,
- lookahead => 30
- );
- if ( $status->{errstr} ) {
- die("get_departures($dep): $status->{errstr}\n");
- }
-
- my ($train) = List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
+ elsif ( $entry->{is_iris} and ( not $backend or $backend eq 'iris' ) ) {
+ eval {
+ if ( $now->epoch - $entry->{real_dep_ts} < 900 ) {
+ my $status = $self->app->iris->get_departures(
+ station => $dep,
+ lookbehind => 30,
+ lookahead => 30
+ );
+ if ( $status->{errstr} ) {
+ die("get_departures($dep): $status->{errstr}\n");
+ }
- if ( not $train ) {
- $self->app->log->debug(
- "could not find train $train_id at $dep\n");
- return;
- }
+ my ($train)
+ = List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- $self->app->in_transit->update_departure(
- uid => $uid,
- train => $train,
- dep_eva => $dep,
- arr_eva => $arr,
- route => [ $self->app->iris->route_diff($train) ]
- );
+ if ( not $train ) {
+ $self->app->log->debug(
+ "could not find train $train_id at $dep\n");
+ return;
+ }
- if ( $train->departure_is_cancelled and $arr ) {
- my $checked_in
- = $self->app->in_transit->update_departure_cancelled(
+ $self->app->in_transit->update_departure(
uid => $uid,
train => $train,
dep_eva => $dep,
arr_eva => $arr,
- );
-
- # depending on the amount of users in transit, some time may
- # have passed between fetching $entry from the database and
- # now. Only check out if the user is still checked into this
- # train.
- if ($checked_in) {
+ route => [ $self->app->iris->route_diff($train) ]
+ );
- # check out (adds a cancelled journey and resets journey state
- # to checkin
- $self->app->checkout_p(
- station => $arr,
- force => 2,
+ if ( $train->departure_is_cancelled and $arr ) {
+ my $checked_in
+ = $self->app->in_transit->update_departure_cancelled(
+ uid => $uid,
+ train => $train,
dep_eva => $dep,
arr_eva => $arr,
- uid => $uid
- )->wait;
+ );
+
+ # depending on the amount of users in transit, some time may
+ # have passed between fetching $entry from the database and
+ # now. Only check out if the user is still checked into this
+ # train.
+ if ($checked_in) {
+
+ # check out (adds a cancelled journey and resets journey state
+ # to checkin
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ }
+ else {
+ $self->app->add_route_timestamps( $uid, $train, 1 );
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $train->train_id,
+ is_departure => 1,
+ eva => $dep,
+ datetime => $train->sched_departure,
+ train_type => $train->type,
+ train_no => $train->train_no
+ );
+ $self->app->add_stationinfo( $uid, 1, $train->train_id,
+ $dep, $arr );
}
}
- else {
- $self->app->add_route_timestamps( $uid, $train, 1 );
- $self->app->add_wagonorder(
- uid => $uid,
- train_id => $train->train_id,
- is_departure => 1,
- eva => $dep,
- datetime => $train->sched_departure,
- train_type => $train->type,
- train_no => $train->train_no
- );
- $self->app->add_stationinfo( $uid, 1, $train->train_id,
- $dep, $arr );
- }
+ };
+ if ($@) {
+ $errors += 1;
+ $self->app->log->error("work($uid) @ IRIS: departure: $@");
}
- };
- if ($@) {
- $errors += 1;
- $self->app->log->error("work($uid) @ IRIS: departure: $@");
- }
- eval {
- if (
- $arr
- and ( not $entry->{real_arr_ts}
- or $now->epoch - $entry->{real_arr_ts} < 600 )
- )
- {
- my $status = $self->app->iris->get_departures(
- station => $arr,
- lookbehind => 20,
- lookahead => 220
- );
- if ( $status->{errstr} ) {
- die("get_departures($arr): $status->{errstr}\n");
- }
+ eval {
+ if (
+ $arr
+ and ( not $entry->{real_arr_ts}
+ or $now->epoch - $entry->{real_arr_ts} < 600 )
+ )
+ {
+ my $status = $self->app->iris->get_departures(
+ station => $arr,
+ lookbehind => 20,
+ lookahead => 220
+ );
+ if ( $status->{errstr} ) {
+ die("get_departures($arr): $status->{errstr}\n");
+ }
- # Note that a train may pass the same station several times.
- # Notable example: S41 / S42 ("Ringbahn") both starts and
- # terminates at Berlin Südkreuz
- my ($train) = List::Util::first {
- $_->train_id eq $train_id
- and $_->sched_arrival
- and $_->sched_arrival->epoch > $entry->{sched_dep_ts}
- }
- @{ $status->{results} };
+ # Note that a train may pass the same station several times.
+ # Notable example: S41 / S42 ("Ringbahn") both starts and
+ # terminates at Berlin Südkreuz
+ my ($train) = List::Util::first {
+ $_->train_id eq $train_id
+ and $_->sched_arrival
+ and $_->sched_arrival->epoch > $entry->{sched_dep_ts}
+ }
+ @{ $status->{results} };
- $train //= List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
+ $train //= List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- if ( not $train ) {
+ if ( not $train ) {
- # If we haven't seen the train yet, its arrival is probably
- # too far in the future. This is not critical.
- return;
- }
+ # If we haven't seen the train yet, its arrival is probably
+ # too far in the future. This is not critical.
+ return;
+ }
- my $checked_in = $self->app->in_transit->update_arrival(
- uid => $uid,
- train => $train,
- route => [ $self->app->iris->route_diff($train) ],
- dep_eva => $dep,
- arr_eva => $arr,
- );
+ my $checked_in = $self->app->in_transit->update_arrival(
+ uid => $uid,
+ train => $train,
+ route => [ $self->app->iris->route_diff($train) ],
+ dep_eva => $dep,
+ arr_eva => $arr,
+ );
- if ( $checked_in and $train->arrival_is_cancelled ) {
+ if ( $checked_in and $train->arrival_is_cancelled ) {
- # check out (adds a cancelled journey and resets journey state
- # to destination selection)
- $self->app->checkout_p(
+ # check out (adds a cancelled journey and resets journey state
+ # to destination selection)
+ $self->app->checkout_p(
+ station => $arr,
+ force => 0,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ else {
+ $self->app->add_route_timestamps(
+ $uid, $train, 0,
+ (
+ defined $entry->{real_arr_ts}
+ and $now->epoch > $entry->{real_arr_ts}
+ ) ? 1 : 0
+ );
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $train->train_id,
+ is_arrival => 1,
+ eva => $arr,
+ datetime => $train->sched_departure,
+ train_type => $train->type,
+ train_no => $train->train_no
+ );
+ $self->app->add_stationinfo( $uid, 0, $train->train_id,
+ $dep, $arr );
+ }
+ }
+ elsif ( $entry->{real_arr_ts} ) {
+ my ( undef, $error ) = $self->app->checkout_p(
station => $arr,
- force => 0,
+ force => 2,
dep_eva => $dep,
arr_eva => $arr,
uid => $uid
+ )->catch(
+ sub {
+ my ($error) = @_;
+ $backend_issues += 1;
+ $self->app->log->error(
+ "work($uid) @ IRIS: arrival: $error");
+ $errors += 1;
+ }
)->wait;
}
- else {
- $self->app->add_route_timestamps(
- $uid, $train, 0,
- (
- defined $entry->{real_arr_ts}
- and $now->epoch > $entry->{real_arr_ts}
- ) ? 1 : 0
- );
- $self->app->add_wagonorder(
- uid => $uid,
- train_id => $train->train_id,
- is_arrival => 1,
- eva => $arr,
- datetime => $train->sched_departure,
- train_type => $train->type,
- train_no => $train->train_no
- );
- $self->app->add_stationinfo( $uid, 0, $train->train_id,
- $dep, $arr );
- }
- }
- elsif ( $entry->{real_arr_ts} ) {
- my ( undef, $error ) = $self->app->checkout_p(
- station => $arr,
- force => 2,
- dep_eva => $dep,
- arr_eva => $arr,
- uid => $uid
- )->catch(
- sub {
- my ($error) = @_;
- $backend_issues += 1;
- $self->app->log->error(
- "work($uid) @ IRIS: arrival: $error");
- $errors += 1;
- }
- )->wait;
+ };
+ if ($@) {
+ $self->app->log->error("work($uid) @ IRIS: arrival: $@");
+ $errors += 1;
}
- };
- if ($@) {
- $self->app->log->error("work($uid) @ IRIS: arrival: $@");
- $errors += 1;
+
+ eval { };
}
- eval { };
}
my $started_at = $now;
@@ -711,15 +736,19 @@ sub run {
my $worker_duration = $main_finished_at->epoch - $started_at->epoch;
if ( $self->app->config->{influxdb}->{url} ) {
+ my $tags = q{};
+ if ($backend) {
+ $tags .= ",backend=${backend}";
+ }
if ( $self->app->mode eq 'development' ) {
$self->app->log->debug( 'POST '
. $self->app->config->{influxdb}->{url}
- . " worker runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}"
+ . " worker${tags} runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}"
);
}
else {
$self->app->ua->post_p( $self->app->config->{influxdb}->{url},
-"worker runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}"
+"worker${tags} runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}"
)->wait;
}
}
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm
index 0978c88..b0722f7 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -345,9 +345,9 @@ sub register {
}
if ( not $dt
- or DateTime->now( time_zone => 'Europe/Berlin' )->epoch - $dt < 6 )
+ or DateTime->now( time_zone => 'Europe/Berlin' )->epoch - $dt < 10 )
{
- # a human user should take at least five seconds to fill out the form.
+ # a human user should take at least ten seconds to fill out the form.
# Throw a CSRF error at presumed spammers.
$self->render(
'bad_request',
@@ -874,6 +874,35 @@ sub webhook {
$self->render( 'webhooks', hook => $hook );
}
+sub change_language {
+ my ($self) = @_;
+
+ my $action = $self->req->param('action');
+ my $language = $self->req->param('language');
+
+ if ( $action and $action eq 'save' ) {
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
+ $self->users->set_language(
+ uid => $self->current_user->{id},
+ language => $language eq 'none' ? undef : $language,
+ );
+ $self->flash( success => 'language' );
+ $self->redirect_to('account');
+ }
+ else {
+ my @languages = @{ $self->current_user->{languages} };
+ $self->param( language => $languages[0] // 'none' );
+ $self->render('language');
+ }
+}
+
sub change_mail {
my ($self) = @_;
@@ -1026,6 +1055,7 @@ sub backend_form {
my ($self) = @_;
my $user = $self->current_user;
+ my %backend_by_id;
my @backends = $self->stations->get_backends;
my @suggested_backends;
@@ -1084,12 +1114,13 @@ sub backend_form {
$backend->{homepage} = $s->{homepage};
$backend->{regions} = [ map { $place_map{$_} // $_ }
@{ $s->{coverage}{regions} // [] } ];
- $backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
- $backend->{experimental} = 1;
+ $backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
+ $backend->{association} = 1;
if (
$s->{coverage}{area}
and $s->{coverage}{area}{type} eq 'Polygon'
+ and defined $user_lon
and $self->lonlat_in_polygon(
$s->{coverage}{area}{coordinates},
[ $user_lon, $user_lat ]
@@ -1099,7 +1130,8 @@ sub backend_form {
push( @suggested_backends, $backend );
}
elsif ( $s->{coverage}{area}
- and $s->{coverage}{area}{type} eq 'MultiPolygon' )
+ and $s->{coverage}{area}{type} eq 'MultiPolygon'
+ and defined $user_lon )
{
for my $s_poly (
@{ $s->{coverage}{area}{coordinates} // [] } )
@@ -1160,6 +1192,7 @@ sub backend_form {
if (
$s->{coverage}{area}
and $s->{coverage}{area}{type} eq 'Polygon'
+ and defined $user_lon
and $self->lonlat_in_polygon(
$s->{coverage}{area}{coordinates},
[ $user_lon, $user_lat ]
@@ -1169,7 +1202,8 @@ sub backend_form {
push( @suggested_backends, $backend );
}
elsif ( $s->{coverage}{area}
- and $s->{coverage}{area}{type} eq 'MultiPolygon' )
+ and $s->{coverage}{area}{type} eq 'MultiPolygon'
+ and defined $user_lon )
{
for my $s_poly (
@{ $s->{coverage}{area}{coordinates} // [] } )
@@ -1211,6 +1245,7 @@ sub backend_form {
if (
$s->{coverage}{area}
and $s->{coverage}{area}{type} eq 'Polygon'
+ and defined $user_lon
and $self->lonlat_in_polygon(
$s->{coverage}{area}{coordinates},
[ $user_lon, $user_lat ]
@@ -1220,7 +1255,8 @@ sub backend_form {
push( @suggested_backends, $backend );
}
elsif ( $s->{coverage}{area}
- and $s->{coverage}{area}{type} eq 'MultiPolygon' )
+ and $s->{coverage}{area}{type} eq 'MultiPolygon'
+ and defined $user_lon )
{
for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } )
{
@@ -1237,8 +1273,14 @@ sub backend_form {
}
}
$backend->{type} = $type;
+
+ $backend_by_id{ $backend->{id} } = $backend;
}
+ my @frequent_backends = grep { $_->{type} }
+ map { $backend_by_id{$_} }
+ $self->journeys->get_frequent_backend_ids( uid => $user->{id} );
+
@backends = map { $_->[1] }
sort { $a->[0] cmp $b->[0] }
map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends;
@@ -1246,6 +1288,7 @@ sub backend_form {
$self->render(
'select_backend',
suggestions => \@suggested_backends,
+ frequent => \@frequent_backends,
backends => \@backends,
user => $user,
redirect_to => $self->req->param('redirect_to') // '/',
diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm
index 572d3fa..fa40e76 100755
--- a/lib/Travelynx/Controller/Api.pm
+++ b/lib/Travelynx/Controller/Api.pm
@@ -188,10 +188,13 @@ sub travel_v1 {
my $to_station = sanitize( q{}, $payload->{toStation} );
my $train_id;
my $dbris = sanitize( undef, $payload->{dbris} );
+ my $efa = sanitize( undef, $payload->{efa} );
my $hafas = sanitize( undef, $payload->{hafas} );
my $motis = sanitize( undef, $payload->{motis} );
- if ( not $hafas and exists $payload->{train}{journeyID} ) {
+ if ( not( $efa or $hafas or $motis )
+ and exists $payload->{train}{journeyID} )
+ {
$dbris //= 'bahn.de';
}
@@ -216,8 +219,7 @@ sub travel_v1 {
return;
}
- if ( not $hafas
- and not $dbris
+ if ( not( $dbris or $efa or $hafas or $motis )
and not $self->stations->search( $from_station, backend_id => 1 ) )
{
$self->render(
@@ -233,8 +235,7 @@ sub travel_v1 {
}
if ( $to_station
- and not $hafas
- and not $dbris
+ and not( $dbris or $efa or $hafas or $motis )
and not $self->stations->search( $to_station, backend_id => 1 ) )
{
$self->render(
@@ -297,8 +298,9 @@ sub travel_v1 {
station => $from_station,
train_id => $train_id,
uid => $uid,
- hafas => $hafas,
dbris => $dbris,
+ efa => $efa,
+ hafas => $hafas,
motis => $motis,
);
}
diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm
index db30d36..978e3f8 100755
--- a/lib/Travelynx/Controller/Profile.pm
+++ b/lib/Travelynx/Controller/Profile.pm
@@ -114,7 +114,8 @@ sub profile {
my $map_data = {};
if ( $status->{checked_in} ) {
$map_data = $self->journeys_to_map_data(
- journeys => [$status],
+ journeys => [$status],
+ with_now_markers => 1,
);
}
@@ -506,7 +507,8 @@ sub user_status {
my $map_data = {};
if ( $status->{checked_in} ) {
$map_data = $self->journeys_to_map_data(
- journeys => [$status],
+ journeys => [$status],
+ with_now_markers => 1,
);
}
@@ -600,7 +602,8 @@ sub status_card {
if ( $status->{checked_in} ) {
$map_data = $self->journeys_to_map_data(
- journeys => [$status],
+ journeys => [$status],
+ with_now_markers => 1,
);
}
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index 40802f4..154938d 100755
--- a/lib/Travelynx/Controller/Traveling.pm
+++ b/lib/Travelynx/Controller/Traveling.pm
@@ -8,13 +8,15 @@ use Mojo::Base 'Mojolicious::Controller';
use DateTime;
use DateTime::Format::Strptime;
+use GIS::Distance;
use List::Util qw(uniq min max);
use List::UtilsBy qw(max_by uniq_by);
-use List::MoreUtils qw(first_index);
+use List::MoreUtils qw(first_index last_index);
use Mojo::UserAgent;
use Mojo::Promise;
use Text::CSV;
use Travel::Status::DE::IRIS::Stations;
+use XML::LibXML;
# Internal Helpers
@@ -370,7 +372,9 @@ sub homepage {
my $map_data = {};
if ( $status->{arr_name} ) {
$map_data = $self->journeys_to_map_data(
- journeys => [$status],
+ journeys => [$status],
+ show_full_route => 1,
+ with_now_markers => 1,
);
}
my $journey_visibility
@@ -460,7 +464,9 @@ sub status_card {
my $map_data = {};
if ( $status->{arr_name} ) {
$map_data = $self->journeys_to_map_data(
- journeys => [$status],
+ journeys => [$status],
+ show_full_route => 1,
+ with_now_markers => 1,
);
}
my $journey_visibility
@@ -588,13 +594,9 @@ sub geolocation {
if ($dbris_service) {
$self->render_later;
- Travel::Status::DE::DBRIS->new_p(
- promise => 'Mojo::Promise',
- user_agent => Mojo::UserAgent->new,
- geoSearch => {
- latitude => $lat,
- longitude => $lon
- }
+ $self->dbris->geosearch_p(
+ latitude => $lat,
+ longitude => $lon
)->then(
sub {
my ($dbris) = @_;
@@ -605,7 +607,7 @@ sub geolocation {
distance => 0,
dbris => $dbris_service,
}
- } $dbris->results;
+ } uniq_by { $_->name } $dbris->results;
if ( @results > 10 ) {
@results = @results[ 0 .. 9 ];
}
@@ -621,8 +623,13 @@ sub geolocation {
$self->render(
json => {
candidates => [],
- warning => $err,
- }
+ error => $err,
+ },
+
+ # The frontend JavaScript does not have an XHR error handler yet
+ # (and if it did, I do not know whether it would have access to our JSON body).
+ # So, for now, we do the bad thing™ and return HTTP 200 even though the request to the backend was not successful.
+ # status => 502,
);
}
)->wait;
@@ -665,8 +672,11 @@ sub geolocation {
$self->render(
json => {
candidates => [],
- warning => $err,
- }
+ error => $err,
+ },
+
+ # See above
+ # status => 502
);
}
)->wait;
@@ -716,8 +726,11 @@ sub geolocation {
$self->render(
json => {
candidates => [],
- warning => $err,
- }
+ error => $err,
+ },
+
+ # See above
+ #status => 502
);
}
)->wait;
@@ -730,6 +743,7 @@ sub geolocation {
Travel::Status::MOTIS->new_p(
promise => 'Mojo::Promise',
user_agent => $self->ua,
+ time_zone => 'Europe/Berlin',
service => $motis_service,
stops_by_coordinate => {
@@ -764,8 +778,11 @@ sub geolocation {
$self->render(
json => {
candidates => [],
- warning => $err,
- }
+ error => $err,
+ },
+
+ # See above
+ #status => 502
);
}
)->wait;
@@ -1157,14 +1174,37 @@ sub station {
$timestamp = DateTime->now( time_zone => 'Europe/Berlin' );
}
- my $dbris_service = $self->param('dbris')
- // ( $user->{backend_dbris} ? $user->{backend_name} : undef );
- my $efa_service = $self->param('efa')
- // ( $user->{backend_efa} ? $user->{backend_name} : undef );
- my $hafas_service = $self->param('hafas')
- // ( $user->{backend_hafas} ? $user->{backend_name} : undef );
- my $motis_service = $self->param('motis')
- // ( $user->{backend_motis} ? $user->{backend_name} : undef );
+ my ( $dbris_service, $efa_service, $hafas_service, $motis_service );
+
+ if ( $self->param('dbris') ) {
+ $dbris_service = $self->param('dbris');
+ }
+ elsif ( $self->param('efa') ) {
+ $efa_service = $self->param('efa');
+ }
+ elsif ( $self->param('hafas') ) {
+ $hafas_service = $self->param('hafas');
+ }
+ elsif ( $self->param('motis') ) {
+ $motis_service = $self->param('motis');
+ }
+ else {
+ if ( $user->{backend_dbris} ) {
+ $dbris_service = $user->{backend_name};
+ }
+ elsif ( $user->{backend_efa} ) {
+ $efa_service = $user->{backend_name};
+ }
+ elsif ( $user->{backend_hafas} ) {
+ $hafas_service = $user->{backend_name};
+ }
+ elsif ( $user->{backend_motis} ) {
+ $motis_service = $user->{backend_name};
+ }
+ }
+
+ my @suggestions;
+
my $promise;
if ($dbris_service) {
if ( $station !~ m{ [@] L = \d+ }x ) {
@@ -1266,6 +1306,37 @@ sub station {
if ( $station =~ m{ [@] O = (?<name> [^@]+ ) [@] }x ) {
$status->{station_name} = $+{name};
}
+
+ my ($eva) = ( $station =~ m{ [@] L = (\d+) }x );
+ my $backend_id
+ = $self->stations->get_backend_id( dbris => $dbris_service );
+ my @destinations = $self->journeys->get_connection_targets(
+ uid => $uid,
+ backend_id => $backend_id,
+ eva => $eva
+ );
+
+ for my $dep (@results) {
+ destination: for my $dest (@destinations) {
+ if ( $dep->destination
+ and $dep->destination eq $dest->{name} )
+ {
+ push( @suggestions, [ $dep, $dest ] );
+ next destination;
+ }
+ for my $via_name ( $dep->via ) {
+ if ( $via_name eq $dest->{name} ) {
+ push( @suggestions, [ $dep, $dest ] );
+ next destination;
+ }
+ }
+ }
+ }
+
+ @suggestions = map { $_->[0] }
+ sort { $a->[1] <=> $b->[1] }
+ grep { $_->[1] >= $now - 300 }
+ map { [ $_, $_->[0]->dep->epoch ] } @suggestions;
}
elsif ($hafas_service) {
@@ -1297,6 +1368,28 @@ sub station {
station_name => $status->stop->full_name,
related_stations => [],
};
+ my $backend_id
+ = $self->stations->get_backend_id( efa => $efa_service );
+ my @destinations = $self->journeys->get_connection_targets(
+ uid => $uid,
+ backend_id => $backend_id,
+ eva => $status->{station_eva},
+ );
+ for my $dep (@results) {
+ destination: for my $dest (@destinations) {
+ for my $stop ( $dep->route_post ) {
+ if ( $stop->full_name eq $dest->{name} ) {
+ push( @suggestions, [ $dep, $dest ] );
+ next destination;
+ }
+ }
+ }
+ }
+
+ @suggestions = map { $_->[0] }
+ sort { $a->[1] <=> $b->[1] }
+ grep { $_->[1] >= $now - 300 and $_->[1] <= $now + 1800 }
+ map { [ $_, $_->[0]->datetime->epoch ] } @suggestions;
}
elsif ($motis_service) {
@results = map { $_->[0] }
@@ -1411,6 +1504,7 @@ sub station {
related_stations => $status->{related_stations},
user_status => $user_status,
can_check_out => $can_check_out,
+ suggestions => \@suggestions,
title => "travelynx: $status->{station_name}",
);
}
@@ -1432,6 +1526,7 @@ sub station {
related_stations => $status->{related_stations},
user_status => $user_status,
can_check_out => $can_check_out,
+ suggestions => \@suggestions,
title => "travelynx: $status->{station_name}",
);
}
@@ -1446,6 +1541,19 @@ sub station {
status => 300,
);
}
+ elsif ( $efa_service
+ and $status
+ and scalar $status->name_candidates )
+ {
+ $self->render(
+ 'disambiguation',
+ suggestions => [
+ map { { name => $_->name, eva => $_->id_num } }
+ $status->name_candidates
+ ],
+ status => 300,
+ );
+ }
elsif ( $hafas_service
and $status
and $status->errcode eq 'LOCATION' )
@@ -1487,7 +1595,7 @@ sub station {
)->wait;
}
elsif ( $err
- =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden}
+ =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden|HTTP 500 Internal Server Error|HTTP 429 Too Many Requests}
)
{
$self->render(
@@ -1710,23 +1818,19 @@ sub map_history {
my $with_polyline = $route_type eq 'beeline' ? 0 : 1;
my $parser = DateTime::Format::Strptime->new(
- pattern => '%d.%m.%Y',
+ pattern => '%F',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
);
- if ( $filter_from
- and $filter_from =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x )
- {
+ if ($filter_from) {
$filter_from = $parser->parse_datetime($filter_from);
}
else {
$filter_from = undef;
}
- if ( $filter_until
- and $filter_until =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x )
- {
+ if ($filter_until) {
$filter_until = $parser->parse_datetime($filter_until)->set(
hour => 23,
minute => 59,
@@ -1804,15 +1908,19 @@ sub csv_history {
my $buf = q{};
$csv->combine(
- qw(Zugtyp Linie Nummer Start Ziel),
- 'Start (DS100)',
- 'Ziel (DS100)',
- 'Abfahrt (soll)',
- 'Abfahrt (ist)',
- 'Ankunft (soll)',
- 'Ankunft (ist)',
- 'Kommentar',
- 'ID'
+ qw(type line number),
+ 'departure stop name',
+ 'departure stop id',
+ 'arrival stop name',
+ 'arrival stop id',
+ 'scheduled departure',
+ 'real-time departure',
+ 'scheduled arrival',
+ 'real-time arrival',
+ 'operator',
+ 'carriage type',
+ 'comment',
+ 'id'
);
$buf .= $csv->string;
@@ -1829,13 +1937,17 @@ sub csv_history {
$journey->{line},
$journey->{no},
$journey->{from_name},
+ $journey->{from_eva},
$journey->{to_name},
- $journey->{from_ds100},
- $journey->{to_ds100},
- $journey->{sched_departure}->strftime('%Y-%m-%d %H:%M'),
- $journey->{rt_departure}->strftime('%Y-%m-%d %H:%M'),
- $journey->{sched_arrival}->strftime('%Y-%m-%d %H:%M'),
- $journey->{rt_arrival}->strftime('%Y-%m-%d %H:%M'),
+ $journey->{to_eva},
+ $journey->{sched_departure}->strftime('%Y-%m-%d %H:%M:%S'),
+ $journey->{rt_departure}->strftime('%Y-%m-%d %H:%M:%S'),
+ $journey->{sched_arrival}->strftime('%Y-%m-%d %H:%M:%S'),
+ $journey->{rt_arrival}->strftime('%Y-%m-%d %H:%M:%S'),
+ $journey->{user_data}{operator} // q{},
+ join( q{ + },
+ map { $_->{desc} // $_->{name} }
+ @{ $journey->{user_data}{wagongroups} // [] } ),
$journey->{user_data}{comment} // q{},
$journey->{id}
)
@@ -2080,11 +2192,17 @@ sub journey_details {
$self->param( journey_id => $journey_id );
if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) {
- $self->render(
- 'journey',
- status => 404,
- error => 'notfound',
- journey => {}
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404
+ },
+ any => {
+ template => 'journey',
+ status => 404,
+ error => 'notfound',
+ journey => {}
+ }
);
return;
}
@@ -2100,6 +2218,40 @@ sub journey_details {
);
if ($journey) {
+
+ if ( $self->stash('polyline_export') ) {
+
+ if ( not( $journey->{polyline} and @{ $journey->{polyline} } ) ) {
+ $journey->{polyline}
+ = [ map { [ $_->[2]{lon}, $_->[2]{lat}, $_->[1] ] }
+ @{ $journey->{route} } ];
+ }
+
+ delete $self->stash->{layout};
+
+ my $xml = $self->render_to_string(
+ template => 'polyline',
+ name => sprintf( '%s %s: %s → %s',
+ $journey->{type}, $journey->{no},
+ $journey->{from_name}, $journey->{to_name} ),
+ polyline => $journey->{polyline}
+ );
+ $self->respond_to(
+ gpx => {
+ text => $xml,
+ format => 'gpx'
+ },
+ json => {
+ json => [
+ map {
+ $_->[2] ? [ $_->[0], $_->[1], int( $_->[2] ) ] : $_
+ } @{ $journey->{polyline} }
+ ]
+ },
+ );
+ return;
+ }
+
my $map_data = $self->journeys_to_map_data(
journeys => [$journey],
include_manual => 1,
@@ -2137,29 +2289,39 @@ sub journey_details {
$delay, $journey->{rt_arrival}->strftime('%H:%M') );
}
- $self->render(
- 'journey',
- title => sprintf(
- 'travelynx: Fahrt %s %s %s am %s',
- $journey->{type}, $journey->{line} // '',
- $journey->{no},
- $journey->{sched_departure}->strftime('%d.%m.%Y um %H:%M')
- ),
- error => undef,
- journey => $journey,
- journey_visibility => $visibility,
- with_map => 1,
- with_share => $with_share,
- share_text => $share_text,
- %{$map_data},
+ $self->respond_to(
+ json => { json => $journey },
+ any => {
+ template => 'journey',
+ title => sprintf(
+ 'travelynx: Fahrt %s %s %s am %s',
+ $journey->{type},
+ $journey->{line} // '',
+ $journey->{no},
+ $journey->{sched_departure}->strftime('%d.%m.%Y um %H:%M')
+ ),
+ error => undef,
+ journey => $journey,
+ journey_visibility => $visibility,
+ with_map => 1,
+ with_share => $with_share,
+ share_text => $share_text,
+ %{$map_data},
+ }
);
}
else {
- $self->render(
- 'journey',
- status => 404,
- error => 'notfound',
- journey => {}
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404
+ },
+ any => {
+ template => 'journey',
+ status => 404,
+ error => 'notfound',
+ journey => {}
+ }
);
}
@@ -2339,7 +2501,12 @@ sub edit_journey {
my $error = undef;
if ( $self->param('action') and $self->param('action') eq 'save' ) {
- my $parser = DateTime::Format::Strptime->new(
+ my $parser_sec = DateTime::Format::Strptime->new(
+ pattern => '%d.%m.%Y %H:%M:%S',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
+ my $parser_min = DateTime::Format::Strptime->new(
pattern => '%d.%m.%Y %H:%M',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
@@ -2350,7 +2517,8 @@ sub edit_journey {
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival))
{
- my $datetime = $parser->parse_datetime( $self->param($key) );
+ my $datetime = $parser_sec->parse_datetime( $self->param($key) )
+ // $parser_min->parse_datetime( $self->param($key) );
if ( $datetime and $datetime->epoch ne $journey->{$key}->epoch ) {
$error = $self->journeys->update(
uid => $uid,
@@ -2371,7 +2539,7 @@ sub edit_journey {
uid => $uid,
db => $db,
id => $journey->{id},
- $key => $self->param($key)
+ $key => $self->param($key),
);
if ($error) {
last;
@@ -2442,8 +2610,14 @@ sub edit_journey {
for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival)) {
if ( $journey->{$key} and $journey->{$key}->epoch ) {
- $self->param(
- $key => $journey->{$key}->strftime('%d.%m.%Y %H:%M') );
+ if ( $journey->{$key}->second ) {
+ $self->param(
+ $key => $journey->{$key}->strftime('%d.%m.%Y %H:%M:%S') );
+ }
+ else {
+ $self->param(
+ $key => $journey->{$key}->strftime('%d.%m.%Y %H:%M') );
+ }
}
}
@@ -2463,17 +2637,228 @@ sub edit_journey {
$self->render(
'edit_journey',
with_autocomplete => 1,
+ backend_id => $journey->{backend_id},
error => $error,
journey => $journey
);
}
+# Taken from Travel::Status::DE::EFA::Trip#polyline
+sub polyline_add_stops {
+ my ( $self, %opt ) = @_;
+
+ my $polyline = $opt{polyline};
+ my $route = $opt{route};
+
+ my $distance = GIS::Distance->new;
+
+ my %min_dist;
+ my $route_i = 0;
+ for my $stop ( @{$route} ) {
+ for my $polyline_index ( 0 .. $#{$polyline} ) {
+ my $pl = $polyline->[$polyline_index];
+ if ( not( defined $stop->[2]{lat} and defined $stop->[2]{lon} ) ) {
+ my $err
+ = sprintf(
+"Cannot match uploaded polyline with the journey's route: route stop %s (ID %s) has no lat/lon\n",
+ $stop->[0], $stop->[1] // 'unknown' );
+ die($err);
+ }
+ my $dist
+ = $distance->distance_metal( $stop->[2]{lat}, $stop->[2]{lon},
+ $pl->[1], $pl->[0] );
+ my $key = $route_i . ';' . $stop->[1];
+ if ( not $min_dist{$key}
+ or $min_dist{$key}{dist} > $dist )
+ {
+ $min_dist{$key} = {
+ dist => $dist,
+ index => $polyline_index,
+ };
+ }
+ }
+ $route_i += 1;
+ }
+ $route_i = 0;
+ for my $stop ( @{$route} ) {
+ my $key = $route_i . ';' . $stop->[1];
+ if ( $min_dist{$key} ) {
+ if ( defined $polyline->[ $min_dist{$key}{index} ][2] ) {
+ return sprintf(
+'Error: Route stops %d and %d both map to polyline lon/lat %f/%f. '
+ . 'The uploaded polyline must cover the following route stops: %s',
+ $polyline->[ $min_dist{$key}{index} ][2],
+ $stop->[1],
+ $polyline->[ $min_dist{$key}{index} ][0],
+ $polyline->[ $min_dist{$key}{index} ][1],
+ join(
+ q{ · },
+ map {
+ sprintf(
+ '%s (ID %s) @ %f/%f',
+ $_->[0], $_->[1] // 'unknown',
+ $_->[2]{lon}, $_->[2]{lat}
+ )
+ } @{$route}
+ ),
+ );
+ }
+ $polyline->[ $min_dist{$key}{index} ][2]
+ = $stop->[1];
+ }
+ $route_i += 1;
+ }
+ return;
+}
+
+sub set_polyline {
+ my ($self) = @_;
+
+ if ( $self->validation->csrf_protect->has_error('csrf_token') ) {
+ $self->render(
+ 'bad_request',
+ csrf => 1,
+ status => 400
+ );
+ return;
+ }
+
+ my $journey_id = $self->param('id');
+ my $uid = $self->current_user->{id};
+
+ # Ensure that the journey exists and belongs to the user
+ my $journey = $self->journeys->get_single(
+ uid => $uid,
+ journey_id => $journey_id,
+ );
+
+ if ( not $journey ) {
+ $self->render(
+ 'bad_request',
+ message => 'Invalid journey ID',
+ status => 400,
+ );
+ return;
+ }
+
+ if ( my $upload = $self->req->upload('file') ) {
+ my $root;
+ eval {
+ $root = XML::LibXML->load_xml( string => $upload->asset->slurp );
+ };
+
+ if ($@) {
+ $self->render(
+ 'bad_request',
+ message => "Invalid GPX file: Invalid XML: $@",
+ status => 400,
+ );
+ return;
+ }
+
+ my $context = XML::LibXML::XPathContext->new($root);
+ $context->registerNs( 'gpx', 'http://www.topografix.com/GPX/1/1' );
+
+ use Data::Dumper;
+
+ my @polyline;
+ for my $point (
+ $context->findnodes('/gpx:gpx/gpx:trk/gpx:trkseg/gpx:trkpt') )
+ {
+ push(
+ @polyline,
+ [
+ 0.0 + $point->getAttribute('lon'),
+ 0.0 + $point->getAttribute('lat')
+ ]
+ );
+ }
+
+ if ( not @polyline ) {
+ $self->render(
+ 'bad_request',
+ message => 'Invalid GPX file: found no track points',
+ status => 400,
+ );
+ return;
+ }
+
+ my @route = @{ $journey->{route} };
+
+ if ( $self->param('upload-partial') ) {
+ my $route_start = first_index {
+ (
+ (
+ $_->[1] and $_->[1] == $journey->{from_eva}
+ or $_->[0] eq $journey->{from_name}
+ )
+ and (
+ not( defined $_->[2]{sched_dep}
+ or defined $_->[2]{rt_dep} )
+ or ( $_->[2]{sched_dep} // $_->[2]{rt_dep} )
+ == $journey->{sched_dep_ts}
+ )
+ )
+ }
+ @route;
+
+ my $route_end = last_index {
+ (
+ (
+ $_->[1] and $_->[1] == $journey->{to_eva}
+ or $_->[0] eq $journey->{to_name}
+ )
+ and (
+ not( defined $_->[2]{sched_arr}
+ or defined $_->[2]{rt_arr} )
+ or ( $_->[2]{sched_arr} // $_->[2]{rt_arr} )
+ == $journey->{sched_arr_ts}
+ )
+ )
+ }
+ @route;
+
+ if ( $route_start > -1 and $route_end > -1 ) {
+ @route = @route[ $route_start .. $route_end ];
+ }
+ }
+
+ my $err = $self->polyline_add_stops(
+ polyline => \@polyline,
+ route => \@route,
+ );
+
+ if ($err) {
+ $self->render(
+ 'bad_request',
+ message => $err,
+ status => 400,
+ );
+ return;
+ }
+
+ $self->journeys->set_polyline(
+ uid => $uid,
+ journey_id => $journey_id,
+ edited => $journey->{edited},
+ polyline => \@polyline,
+ from_eva => $route[0][1],
+ to_eva => $route[-1][1],
+ stats_ts => $journey->{rt_dep_ts},
+ );
+ }
+
+ $self->redirect_to("/journey/${journey_id}");
+}
+
sub add_journey_form {
my ($self) = @_;
+ $self->stash( backend_id => $self->current_user->{backend_id} );
+
if ( $self->param('action') and $self->param('action') eq 'save' ) {
my $parser = DateTime::Format::Strptime->new(
- pattern => '%d.%m.%Y %H:%M',
+ pattern => '%FT%H:%M',
locale => 'de_DE',
time_zone => 'Europe/Berlin'
);
@@ -2493,7 +2878,7 @@ sub add_journey_form {
with_autocomplete => 1,
status => 400,
error =>
-'Zug muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
+'Fahrt muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
);
return;
}
@@ -2531,7 +2916,7 @@ sub add_journey_form {
$opt{db} = $db;
$opt{uid} = $self->current_user->{id};
- $opt{backend_id} = 1;
+ $opt{backend_id} = $self->current_user->{backend_id};
my ( $journey_id, $error ) = $self->journeys->add(%opt);
@@ -2567,4 +2952,269 @@ sub add_journey_form {
}
}
+sub add_intransit_form {
+ my ($self) = @_;
+
+ $self->stash( backend_id => $self->current_user->{backend_id} );
+
+ if ( $self->param('action') and $self->param('action') eq 'save' ) {
+ my $parser = DateTime::Format::Strptime->new(
+ pattern => '%FT%H:%M',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
+ my $time_parser = DateTime::Format::Strptime->new(
+ pattern => '%H:%M',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
+ my %opt;
+ my %trip;
+
+ my @parts = split( qr{\s+}, $self->param('train') );
+
+ if ( @parts == 2 ) {
+ @trip{ 'train_type', 'train_no' } = @parts;
+ }
+ elsif ( @parts == 3 ) {
+ @trip{ 'train_type', 'train_line', 'train_no' } = @parts;
+ }
+ else {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error =>
+'Fahrt muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
+ );
+ return;
+ }
+
+ for my $key (qw(sched_departure sched_arrival)) {
+ if ( $self->param($key) ) {
+ my $datetime = $parser->parse_datetime( $self->param($key) );
+ if ( not $datetime ) {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => "${key}: Ungültiges Datums-/Zeitformat"
+ );
+ return;
+ }
+ $trip{$key} = $datetime;
+ }
+ }
+
+ for my $key (qw(dep_station arr_station route comment)) {
+ $trip{$key} = $self->param($key);
+ }
+
+ $opt{backend_id} = $self->current_user->{backend_id};
+
+ my $dep_stop = $self->stations->search( $trip{dep_station},
+ backend_id => $opt{backend_id} );
+ my $arr_stop = $self->stations->search( $trip{arr_station},
+ backend_id => $opt{backend_id} );
+
+ if ( defined $trip{route} ) {
+ $trip{route} = [ split( qr{\r?\n\r?}, $trip{route} ) ];
+ }
+
+ my $route_has_start = 0;
+ my $route_has_stop = 0;
+
+ for my $station ( @{ $trip{route} || [] } ) {
+ if ( $station eq $dep_stop->{name}
+ or $station eq $dep_stop->{eva} )
+ {
+ $route_has_start = 1;
+ }
+ if ( $station eq $arr_stop->{name}
+ or $station eq $arr_stop->{eva} )
+ {
+ $route_has_stop = 1;
+ }
+ }
+
+ my @route;
+
+ if ( not $route_has_start ) {
+ push(
+ @route,
+ [
+ $dep_stop->{name},
+ $dep_stop->{eva},
+ {
+ lat => $dep_stop->{lat},
+ lon => $dep_stop->{lon},
+ }
+ ]
+ );
+ }
+
+ if ( $trip{route} ) {
+ my @unknown_stations;
+ my $prev_ts = $trip{sched_departure};
+ for my $station ( @{ $trip{route} } ) {
+ my $ts;
+ my %station_data;
+ if ( $station
+ =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x
+ )
+ {
+ $station = $+{stop};
+
+ # attempt to parse "07:08" short timestamp first
+ $ts = $time_parser->parse_datetime( $+{timestamp} );
+ if ($ts) {
+
+ # fill in last stop's (or at the first stop, our departure's)
+ # date to complete the datetime
+ $ts = $ts->set(
+ year => $prev_ts->year,
+ month => $prev_ts->month,
+ day => $prev_ts->day
+ );
+
+ # if we go back in time with this, assume we went
+ # over midnight and add a day, e.g. in case of a stop
+ # at 23:00 followed by one at 01:30
+ if ( $ts < $prev_ts ) {
+ $ts = $ts->add( days => 1 );
+ }
+ }
+ else {
+ # do a full datetime parse
+ $ts = $parser->parse_datetime( $+{timestamp} );
+ }
+ if ( $ts and $ts >= $prev_ts ) {
+ $station_data{sched_arr} = $ts->epoch;
+ $station_data{sched_dep} = $ts->epoch;
+ $prev_ts = $ts;
+ }
+ else {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => "Ungültige Zeitangabe: $+{timestamp}"
+ );
+ return;
+ }
+ }
+ my $station_info = $self->stations->search( $station,
+ backend_id => $opt{backend_id} );
+ if ($station_info) {
+ $station_data{lat} = $station_info->{lat};
+ $station_data{lon} = $station_info->{lon};
+ push(
+ @route,
+ [
+ $station_info->{name}, $station_info->{eva},
+ \%station_data,
+ ]
+ );
+ }
+ else {
+ push( @route, [ $station, undef, {} ] );
+ push( @unknown_stations, $station );
+ }
+ }
+
+ if ( @unknown_stations == 1 ) {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => "Unbekannter Unterwegshalt: $unknown_stations[0]"
+ );
+ return;
+ }
+ elsif (@unknown_stations) {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => 'Unbekannte Unterwegshalte: '
+ . join( ', ', @unknown_stations )
+ );
+ return;
+ }
+ }
+
+ if ( not $route_has_stop ) {
+ push(
+ @route,
+ [
+ $arr_stop->{name},
+ $arr_stop->{eva},
+ {
+ lat => $arr_stop->{lat},
+ lon => $arr_stop->{lon},
+ }
+ ]
+ );
+ }
+
+ for my $station (@route) {
+ if ( $station->[0] eq $dep_stop->{name}
+ or $station->[1] eq $dep_stop->{eva} )
+ {
+ $station->[2]{sched_dep} = $trip{sched_departure}->epoch;
+ }
+ if ( $station->[0] eq $arr_stop->{name}
+ or $station->[1] eq $arr_stop->{eva} )
+ {
+ $station->[2]{sched_arr} = $trip{sched_arrival}->epoch;
+ }
+ }
+
+ my $error;
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+
+ $trip{dep_id} = $dep_stop->{eva};
+ $trip{arr_id} = $arr_stop->{eva};
+ $trip{route} = \@route;
+
+ $opt{db} = $db;
+ $opt{manual} = \%trip;
+ $opt{uid} = $self->current_user->{id};
+
+ if ( not defined $trip{dep_id} ) {
+ $error = "Unknown departure stop '$trip{dep_station}'";
+ }
+ elsif ( not defined $trip{arr_id} ) {
+ $error = "Unknown arrival stop '$trip{arr_station}'";
+ }
+ elsif ( $trip{sched_arrival} <= $trip{sched_departure} ) {
+ $error = 'Ankunftszeit muss nach Abfahrtszeit liegen';
+ }
+ else {
+ $error = $self->in_transit->add(%opt);
+ }
+
+ if ($error) {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => $error,
+ );
+ }
+ else {
+ $tx->commit;
+ $self->redirect_to('/');
+ }
+ }
+ else {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ error => undef
+ );
+ }
+}
+
1;
diff --git a/lib/Travelynx/Helper/DBDB.pm b/lib/Travelynx/Helper/DBDB.pm
deleted file mode 100644
index a310aa3..0000000
--- a/lib/Travelynx/Helper/DBDB.pm
+++ /dev/null
@@ -1,201 +0,0 @@
-package Travelynx::Helper::DBDB;
-
-# Copyright (C) 2020-2023 Birte Kristina Friesel
-#
-# SPDX-License-Identifier: AGPL-3.0-or-later
-
-use strict;
-use warnings;
-use 5.020;
-
-use Encode qw(decode);
-use Mojo::Promise;
-use JSON;
-
-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 has_wagonorder_p {
- my ( $self, %opt ) = @_;
-
- $opt{train_type} //= q{};
- my $datetime = $opt{datetime}->clone->set_time_zone('UTC');
- my %param = (
- administrationId => 80,
- category => $opt{train_type},
- date => $datetime->strftime('%Y-%m-%d'),
- evaNumber => $opt{eva},
- number => $opt{train_no},
- time => $datetime->rfc3339 =~ s{(?=Z)}{.000}r
- );
-
- my $url = sprintf( '%s?%s',
-'https://www.bahn.de/web/api/reisebegleitung/wagenreihung/vehicle-sequence',
- join( '&', map { $_ . '=' . $param{$_} } sort keys %param ) );
-
- my $promise = Mojo::Promise->new;
- my $debug_prefix
- = "has_wagonorder_p($opt{train_type} $opt{train_no} @ $opt{eva})";
-
- if ( my $content = $self->{main_cache}->get("HEAD $url")
- // $self->{realtime_cache}->get("HEAD $url") )
- {
- if ( $content eq 'n' ) {
- $self->{log}->debug("${debug_prefix}: n (cached)");
- return $promise->reject;
- }
- else {
- $self->{log}->debug("${debug_prefix}: ${content} (cached)");
- return $promise->resolve($content);
- }
- }
-
- $self->{user_agent}->request_timeout(5)
- ->get_p( $url => $self->{header} )
- ->then(
- sub {
- my ($tx) = @_;
- if ( $tx->result->is_success ) {
- $self->{log}->debug("${debug_prefix}: a");
- $self->{main_cache}->set( "HEAD $url", 'a' );
- my $body = decode( 'utf-8', $tx->res->body );
- my $json = JSON->new->decode($body);
- $self->{main_cache}->freeze( $url, $json );
- $promise->resolve('a');
- }
- else {
- my $code = $tx->res->code;
- $self->{log}->debug("${debug_prefix}: n (HTTP $code)");
- $self->{realtime_cache}->set( "HEAD $url", 'n' );
- $promise->reject;
- }
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->{log}->debug("${debug_prefix}: n ($err)");
- $self->{realtime_cache}->set( "HEAD $url", 'n' );
- $promise->reject;
- return;
- }
- )->wait;
- return $promise;
-}
-
-sub get_wagonorder_p {
- my ( $self, %opt ) = @_;
-
- my $datetime = $opt{datetime}->clone->set_time_zone('UTC');
- my %param = (
- administrationId => 80,
- category => $opt{train_type},
- date => $datetime->strftime('%Y-%m-%d'),
- evaNumber => $opt{eva},
- number => $opt{train_no},
- time => $datetime->rfc3339 =~ s{(?=Z)}{.000}r
- );
-
- my $url = sprintf( '%s?%s',
-'https://www.bahn.de/web/api/reisebegleitung/wagenreihung/vehicle-sequence',
- join( '&', map { $_ . '=' . $param{$_} } sort keys %param ) );
- my $debug_prefix
- = "get_wagonorder_p($opt{train_type} $opt{train_no} @ $opt{eva})";
-
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $self->{main_cache}->thaw($url) ) {
- $self->{log}->debug("${debug_prefix}: (cached)");
- $promise->resolve($content);
- return $promise;
- }
-
- $self->{user_agent}->request_timeout(5)
- ->get_p( $url => $self->{header} )
- ->then(
- sub {
- my ($tx) = @_;
-
- if ( $tx->result->is_success ) {
- my $body = decode( 'utf-8', $tx->res->body );
- my $json = JSON->new->decode($body);
- $self->{log}->debug("${debug_prefix}: success");
- $self->{main_cache}->freeze( $url, $json );
- $promise->resolve($json);
- }
- else {
- my $code = $tx->res->code;
- $self->{log}->debug("${debug_prefix}: HTTP ${code}");
- $promise->reject("HTTP ${code}");
- }
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->{log}->debug("${debug_prefix}: error ${err}");
- $promise->reject($err);
- return;
- }
- )->wait;
- return $promise;
-}
-
-sub get_stationinfo_p {
- my ( $self, $eva ) = @_;
-
- my $url = "https://lib.finalrewind.org/dbdb/s/${eva}.json";
-
- my $cache = $self->{main_cache};
- my $promise = Mojo::Promise->new;
-
- if ( my $content = $cache->thaw($url) ) {
- $self->{log}->debug("get_stationinfo_p(${eva}): (cached)");
- return $promise->resolve($content);
- }
-
- $self->{user_agent}->request_timeout(5)
- ->get_p( $url => $self->{header} )
- ->then(
- sub {
- my ($tx) = @_;
-
- if ( my $err = $tx->error ) {
- $self->{log}->debug(
-"get_stationinfo_p(${eva}): HTTP $err->{code} $err->{message}"
- );
- $cache->freeze( $url, {} );
- $promise->reject("HTTP $err->{code} $err->{message}");
- return;
- }
-
- my $json = $tx->result->json;
- $self->{log}->debug("get_stationinfo_p(${eva}): success");
- $cache->freeze( $url, $json );
- $promise->resolve($json);
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->{log}->debug("get_stationinfo_p(${eva}): Error ${err}");
- $cache->freeze( $url, {} );
- $promise->reject($err);
- return;
- }
- )->wait;
- return $promise;
-}
-
-1;
diff --git a/lib/Travelynx/Helper/DBRIS.pm b/lib/Travelynx/Helper/DBRIS.pm
index 1b7f099..deeed65 100644
--- a/lib/Travelynx/Helper/DBRIS.pm
+++ b/lib/Travelynx/Helper/DBRIS.pm
@@ -29,18 +29,54 @@ sub new {
return bless( \%opt, $class );
}
+sub get_agent {
+ my ($self) = @_;
+
+ my $agent = $self->{user_agent};
+ my $proxy;
+ if ( my @proxies = @{ $self->{service_config}{'bahn.de'}{proxies} // [] } )
+ {
+ $proxy = $proxies[ int( rand( scalar @proxies ) ) ];
+ }
+ elsif ( my $p = $self->{service_config}{'bahn.de'}{proxy} ) {
+ $proxy = $p;
+ }
+
+ if ($proxy) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
+ return $agent;
+}
+
+sub geosearch_p {
+ my ( $self, %opt ) = @_;
+
+ return Travel::Status::DE::DBRIS->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => $self->get_agent,
+ geoSearch => \%opt,
+ developer_mode => $self->{log}->is_level('debug') ? 1 : 0,
+ );
+}
+
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},
+ cache => $self->{realtime_cache},
lwp_options => {
timeout => 10,
agent => $self->{header}{'User-Agent'},
},
- promise => 'Mojo::Promise',
- user_agent => Mojo::UserAgent->new,
+ promise => 'Mojo::Promise',
+ user_agent => $self->get_agent,
+ developer_mode => $self->{log}->is_level('debug') ? 1 : 0,
)->then(
sub {
my ($dbris) = @_;
@@ -67,8 +103,6 @@ sub get_station_id_p {
sub get_departures_p {
my ( $self, %opt ) = @_;
- my $agent = $self->{user_agent};
-
if ( $opt{station} =~ m{ [@] L = (?<eva> \d+ ) }x ) {
$opt{station} = {
eva => $+{eva},
@@ -81,12 +115,15 @@ sub get_departures_p {
? $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),
+ station => $opt{station},
+ datetime => $when,
+ num_vias => 42,
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->get_agent->request_timeout(10),
+ developer_mode => $self->{log}->is_level('debug') ? 1 : 0,
);
}
@@ -95,28 +132,13 @@ sub get_journey_p {
my $promise = Mojo::Promise->new;
- my $agent = $self->{user_agent};
- my $proxy;
- if ( my @proxies = @{ $self->{service_config}{'bahn.de'}{proxies} // [] } )
- {
- $proxy = $proxies[ int( rand( scalar @proxies ) ) ];
- }
- elsif ( my $p = $self->{service_config}{'bahn.de'}{proxy} ) {
- $proxy = $p;
- }
-
- if ($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),
+ journey => $opt{trip_id},
+ with_polyline => $opt{with_polyline},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->get_agent->request_timeout(10),
+ developer_mode => $self->{log}->is_level('debug') ? 1 : 0,
)->then(
sub {
my ($dbris) = @_;
@@ -143,4 +165,25 @@ sub get_journey_p {
return $promise;
}
+sub get_wagonorder_p {
+ my ( $self, %opt ) = @_;
+
+ $self->{log}
+ ->debug("get_wagonorder_p($opt{train_type} $opt{train_no} @ $opt{eva})");
+
+ return Travel::Status::DE::DBRIS->new_p(
+ cache => $self->{main_cache},
+ failure_cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->get_agent->request_timeout(10),
+ formation => {
+ departure => $opt{datetime},
+ eva => $opt{eva},
+ train_type => $opt{train_type},
+ train_number => $opt{train_no}
+ },
+ developer_mode => $self->{log}->is_level('debug') ? 1 : 0,
+ );
+}
+
1;
diff --git a/lib/Travelynx/Helper/Locales.pm b/lib/Travelynx/Helper/Locales.pm
new file mode 100644
index 0000000..d5ccb71
--- /dev/null
+++ b/lib/Travelynx/Helper/Locales.pm
@@ -0,0 +1,28 @@
+package Travelynx::Helper::Locales;
+
+use strict;
+use warnings;
+
+#BEGIN { package Locale::Maketext; sub DEBUG() {1} };
+#BEGIN { package Locale::Maketext::Guts; sub DEBUG() {1} };
+
+use base qw(Locale::Maketext);
+
+# Uncomment this to show raw strings for untranslated content rather than
+# falling back to German.
+
+#our %Lexicon = (
+# _AUTO => 1,
+#);
+
+use Locale::Maketext::Lexicon {
+ _decode => 1,
+ '*' => [ Gettext => 'share/locales/*.po' ],
+};
+
+sub init {
+ my ($self) = @_;
+ return $self->SUPER::init( @_[ 1 .. $#_ ] );
+}
+
+1;
diff --git a/lib/Travelynx/Helper/MOTIS.pm b/lib/Travelynx/Helper/MOTIS.pm
index d4e1777..df79385 100644
--- a/lib/Travelynx/Helper/MOTIS.pm
+++ b/lib/Travelynx/Helper/MOTIS.pm
@@ -53,6 +53,7 @@ sub get_station_by_query_p {
cache => $self->{cache},
promise => 'Mojo::Promise',
user_agent => Mojo::UserAgent->new,
+ time_zone => 'Europe/Berlin',
lwp_options => {
timeout => 10,
agent => $self->{header}{'User-Agent'},
@@ -101,6 +102,7 @@ sub get_departures_p {
cache => $self->{cache},
promise => 'Mojo::Promise',
user_agent => Mojo::UserAgent->new,
+ time_zone => 'Europe/Berlin',
lwp_options => {
timeout => 10,
agent => $self->{header}{'User-Agent'},
@@ -125,6 +127,7 @@ sub get_trip_p {
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
user_agent => Mojo::UserAgent->new,
+ time_zone => 'Europe/Berlin',
service => $opt{service},
trip_id => $opt{trip_id},
diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm
index b67b716..8324027 100644
--- a/lib/Travelynx/Model/InTransit.pm
+++ b/lib/Travelynx/Model/InTransit.pm
@@ -10,6 +10,7 @@ use warnings;
use 5.020;
use DateTime;
+use GIS::Distance;
use JSON;
my %visibility_itoa = (
@@ -101,6 +102,7 @@ sub add {
my $journey = $opt{journey};
my $stop = $opt{stop};
my $stopover = $opt{stopover};
+ my $manual = $opt{manual};
my $checkin_station_id = $opt{departure_eva};
my $route = $opt{route};
my $data = $opt{data};
@@ -129,7 +131,7 @@ sub add {
messages => $json->encode(
[ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ]
),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $train->departure_has_realtime ? 1
: 0,
@@ -152,19 +154,22 @@ sub add {
$j_stop->full_name,
$j_stop->id_num,
{
- sched_arr => _epoch( $j_stop->sched_arr ),
- sched_dep => _epoch( $j_stop->sched_dep ),
- rt_arr => _epoch( $j_stop->rt_arr ),
- rt_dep => _epoch( $j_stop->rt_dep ),
- isCancelled => $j_stop->is_cancelled,
- arr_delay => $j_stop->arr_delay,
- dep_delay => $j_stop->dep_delay,
- efa_load => $j_stop->occupancy,
- lat => $j_stop->latlon->[0],
- lon => $j_stop->latlon->[1],
+ 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,
+ platform => $j_stop->platform,
+ efa_load => $j_stop->occupancy,
+ lat => $j_stop->latlon->[0],
+ lon => $j_stop->latlon->[1],
}
]
);
+ if ( $j_stop->is_cancelled ) {
+ $route[-1][2]{isCancelled} = 1;
+ }
}
$persistent_data->{operator} = $journey->operator;
$db->insert(
@@ -182,13 +187,13 @@ sub add {
sched_departure => $stop->sched_dep,
real_departure => $stop->rt_dep // $stop->sched_dep,
route => $json->encode( \@route ),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $stop->rt_dep ? 1 : 0,
%{ $data // {} }
}
),
- user_data => JSON->new->encode($persistent_data),
+ user_data => $json->encode($persistent_data),
backend_id => $backend_id,
}
);
@@ -213,6 +218,7 @@ sub add {
rt_dep => _epoch( $j_stop->rt_dep ),
arr_delay => $j_stop->arr_delay,
dep_delay => $j_stop->dep_delay,
+ platform => $j_stop->platform,
load => $j_stop->load,
lat => $j_stop->loc->lat,
lon => $j_stop->loc->lon,
@@ -243,13 +249,13 @@ sub add {
sched_departure => $stop->{sched_dep},
real_departure => $stop->{rt_dep} // $stop->{sched_dep},
route => $json->encode( \@route ),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $stop->{rt_dep} ? 1 : 0,
%{ $data // {} }
}
),
- user_data => JSON->new->encode($persistent_data),
+ user_data => $json->encode($persistent_data),
backend_id => $backend_id,
}
);
@@ -258,7 +264,11 @@ sub add {
and $stop
and ref($journey) eq 'Travel::Status::DE::DBRIS::Journey' )
{
- my $number = $journey->train_no // $journey->number // $train_suffix;
+ my $trip_no
+ = $journey->trip_no_at( $stop->eva,
+ $stop->sched_dep ? $stop->sched_dep->epoch : undef )
+ // $journey->train_no;
+ my $number = $trip_no // $journey->number // $train_suffix;
my $line;
if ( defined $journey->line_no and $journey->line_no ne $number ) {
@@ -276,14 +286,14 @@ sub add {
$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 ),
- isCancelled => $j_stop->is_cancelled,
- arr_delay => $j_stop->arr_delay,
- dep_delay => $j_stop->dep_delay,
- load => {
+ 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,
+ platform => $j_stop->platform,
+ load => {
FIRST => $j_stop->occupancy_first,
SECOND => $j_stop->occupancy_second
},
@@ -292,6 +302,12 @@ sub add {
}
]
);
+ if ( $j_stop->is_additional ) {
+ $route[-1][2]{isAdditional} = 1;
+ }
+ if ( $j_stop->is_cancelled ) {
+ $route[-1][2]{isCancelled} = 1;
+ }
}
my @messages;
for my $msg ( $journey->messages ) {
@@ -313,6 +329,12 @@ sub add {
);
}
}
+ if ( scalar $journey->admin_ids ) {
+ $persistent_data->{admin_ids} = [ $journey->admin_ids ];
+ }
+ if ( scalar $journey->operators ) {
+ $persistent_data->{operators} = [ $journey->operators ];
+ }
$db->insert(
'in_transit',
{
@@ -330,13 +352,13 @@ sub add {
sched_departure => $stop->sched_dep,
real_departure => $stop->rt_dep // $stop->sched_dep,
route => $json->encode( \@route ),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $stop->{rt_dep} ? 1 : 0,
%{ $data // {} }
}
),
- user_data => JSON->new->encode($persistent_data),
+ user_data => $json->encode($persistent_data),
backend_id => $backend_id,
}
);
@@ -363,6 +385,7 @@ sub add {
_epoch( $journey_stopover->realtime_departure ),
arr_delay => $journey_stopover->arrival_delay,
dep_delay => $journey_stopover->departure_delay,
+ platform => $journey_stopover->track,
lat => $journey_stopover->stop->lat,
lon => $journey_stopover->stop->lon,
}
@@ -389,17 +412,50 @@ sub add {
sched_departure => $stopover->scheduled_departure,
real_departure => $stopover->departure,
route => $json->encode( \@route ),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $stopover->{is_realtime} ? 1 : 0,
%{ $data // {} }
}
),
- user_data => JSON->new->encode($persistent_data),
+ user_data => $json->encode($persistent_data),
backend_id => $backend_id,
}
);
}
+ elsif ($manual) {
+ if ( $manual->{comment} ) {
+ $persistent_data->{comment} = $manual->{comment};
+ }
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => 0,
+ checkin_station_id => $manual->{dep_id},
+ checkout_station_id => $manual->{arr_id},
+ checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ),
+ train_type => $manual->{train_type},
+ train_no => $manual->{train_no} || q{},
+ train_id => 'manual',
+ train_line => $manual->{train_line} || undef,
+ sched_departure => $manual->{sched_departure},
+ real_departure => $manual->{sched_departure},
+ sched_arrival => $manual->{sched_arrival},
+ real_arrival => $manual->{sched_arrival},
+ route => $json->encode( $manual->{route} // [] ),
+ data => $json->encode(
+ {
+ manual => \1,
+ %{ $data // {} }
+ }
+ ),
+ user_data => $json->encode($persistent_data),
+ backend_id => $backend_id,
+ }
+ );
+ return;
+ }
else {
die('invalid arguments / argument types passed to InTransit->add');
}
@@ -478,6 +534,14 @@ sub postprocess {
$ret->{comment} = $ret->{user_data}{comment};
$ret->{wagongroups} = $ret->{user_data}{wagongroups};
+ if ( $ret->{sched_dep_ts} and $ret->{real_dep_ts} ) {
+ $ret->{dep_delay} = $ret->{real_dep_ts} - $ret->{sched_dep_ts};
+ }
+
+ if ( $ret->{sched_arr_ts} and $ret->{real_arr_ts} ) {
+ $ret->{arr_delay} = $ret->{real_arr_ts} - $ret->{sched_arr_ts};
+ }
+
$ret->{platform_type} = 'Gleis';
if ( $ret->{train_type} and $ret->{train_type} =~ m{ ast | bus | ruf }ix ) {
$ret->{platform_type} = 'Steig';
@@ -595,6 +659,7 @@ sub get {
if ( $opt{with_polyline} and $ret ) {
$ret->{dep_latlon} = [ $ret->{dep_lat}, $ret->{dep_lon} ];
$ret->{arr_latlon} = [ $ret->{arr_lat}, $ret->{arr_lon} ];
+ $ret->{now_latlon} = $self->estimate_trip_position($ret);
}
if ( $opt{with_visibility} and $ret ) {
@@ -736,6 +801,22 @@ sub set_arrival_eva {
);
}
+sub set_arrival_platform {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $platform = $opt{arrival_platform};
+
+ $db->update(
+ 'in_transit',
+ {
+ arr_platform => $platform,
+ },
+ { user_id => $uid }
+ );
+}
+
sub set_arrival_times {
my ( $self, %opt ) = @_;
@@ -1193,15 +1274,15 @@ sub update_arrival_dbris {
$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 ),
- platform => $j_stop->platform,
- isCancelled => $j_stop->is_cancelled,
- arr_delay => $j_stop->arr_delay,
- dep_delay => $j_stop->dep_delay,
- load => {
+ 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 ),
+ platform => $j_stop->platform,
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ platform => $j_stop->platform,
+ load => {
FIRST => $j_stop->occupancy_first,
SECOND => $j_stop->occupancy_second
},
@@ -1210,6 +1291,12 @@ sub update_arrival_dbris {
}
]
);
+ if ( $j_stop->is_additional ) {
+ $route[-1][2]{isAdditional} = 1;
+ }
+ if ( $j_stop->is_cancelled ) {
+ $route[-1][2]{isCancelled} = 1;
+ }
}
# selecting on user_id and train_no avoids a race condition if a user checks
@@ -1218,8 +1305,8 @@ sub update_arrival_dbris {
$db->update(
'in_transit',
{
- real_arrival => $stop->{rt_arr},
- arr_platform => $stop->{platform},
+ real_arrival => $stop->rt_arr,
+ arr_platform => $stop->platform,
route => $json->encode( [@route] ),
data => $json->encode($ephemeral_data),
user_data => $json->encode($persistent_data),
@@ -1261,21 +1348,27 @@ sub update_arrival_efa {
$j_stop->full_name,
$j_stop->id_num,
{
- sched_arr => _epoch( $j_stop->sched_arr ),
- sched_dep => _epoch( $j_stop->sched_dep ),
- rt_arr => _epoch( $j_stop->rt_arr ),
- rt_dep => _epoch( $j_stop->rt_dep ),
- isCancelled => $j_stop->is_cancelled,
- arr_delay => $j_stop->arr_delay,
- dep_delay => $j_stop->dep_delay,
- efa_load => $j_stop->occupancy,
- lat => $j_stop->latlon->[0],
- lon => $j_stop->latlon->[1],
+ 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,
+ platform => $j_stop->platform,
+ efa_load => $j_stop->occupancy,
+ lat => $j_stop->latlon->[0],
+ lon => $j_stop->latlon->[1],
}
]
);
+ if ( $j_stop->is_cancelled ) {
+ $route[-1][2]{isCancelled} = 1;
+ }
}
+ # TODO set efa_load from old route entry if missing in current route entry
+ # (at least in VVO, occupancy data is only provided for future stops)
+
# 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.
@@ -1284,6 +1377,7 @@ sub update_arrival_efa {
{
data => $json->encode($ephemeral_data),
real_arrival => $stop->rt_arr,
+ arr_platform => $stop->platform,
route => $json->encode( [@route] ),
},
{
@@ -1321,6 +1415,7 @@ sub update_arrival_motis {
rt_dep => _epoch( $journey_stopover->realtime_departure ),
arr_delay => $journey_stopover->arrival_delay,
dep_delay => $journey_stopover->departure_delay,
+ platform => $journey_stopover->track,
lat => $journey_stopover->stop->lat,
lon => $journey_stopover->stop->lon,
}
@@ -1334,7 +1429,8 @@ sub update_arrival_motis {
$db->update(
'in_transit',
{
- real_arrival => $stopover->{realtime_arrival},
+ real_arrival => $stopover->realtime_arrival,
+ arr_platform => $stopover->track,
route => $json->encode( [@route] ),
},
{
@@ -1380,6 +1476,7 @@ sub update_arrival_hafas {
rt_dep => _epoch( $j_stop->rt_dep ),
arr_delay => $j_stop->arr_delay,
dep_delay => $j_stop->dep_delay,
+ platform => $j_stop->platform,
load => $j_stop->load,
lat => $j_stop->loc->lat,
lon => $j_stop->loc->lon,
@@ -1406,7 +1503,8 @@ sub update_arrival_hafas {
'in_transit',
{
data => $json->encode($ephemeral_data),
- real_arrival => $stop->{rt_arr},
+ real_arrival => $stop->rt_arr,
+ arr_platform => $stop->platform,
route => $json->encode( [@route] ),
},
{
@@ -1488,4 +1586,141 @@ sub update_visibility {
);
}
+sub estimate_trip_position_between_stops {
+ my ( $self, %opt ) = @_;
+
+ my $time_complete = $opt{now} - $opt{from_ts};
+ my $time_total = $opt{to_ts} - $opt{from_ts};
+ my $ratio = $time_complete / $time_total;
+
+ my $distance = GIS::Distance->new;
+ my $polyline = $opt{polyline};
+ my ( $i_from, $i_to );
+
+ for my $i ( 0 .. $#{$polyline} ) {
+ if ( not defined $i_from
+ and $polyline->[$i][2]
+ and $polyline->[$i][2] == $opt{from}[1] )
+ {
+ $i_from = $i;
+ }
+ elsif ( not defined $i_to
+ and $polyline->[$i][2]
+ and $polyline->[$i][2] == $opt{to}[1] )
+ {
+ $i_to = $i;
+ last;
+ }
+ }
+ if ( defined $i_from and defined $i_to ) {
+ my $total_distance = 0;
+ for my $i ( $i_from + 1 .. $i_to ) {
+ my $prev = $polyline->[ $i - 1 ];
+ my $this = $polyline->[$i];
+ if ( $prev and $this ) {
+ $total_distance
+ += $distance->distance_metal( $prev->[1], $prev->[0],
+ $this->[1], $this->[0] );
+ }
+ }
+
+ my $marker_distance = $total_distance * $ratio;
+ $total_distance = 0;
+ for my $i ( $i_from + 1 .. $i_to ) {
+ my $prev = $polyline->[ $i - 1 ];
+ my $this = $polyline->[$i];
+ if ( $prev and $this ) {
+ my $prev_distance = $total_distance;
+ $total_distance
+ += $distance->distance_metal( $prev->[1], $prev->[0],
+ $this->[1], $this->[0] );
+ if ( $total_distance > $marker_distance ) {
+ my $sub_ratio = 1;
+ if ( $total_distance != $prev_distance ) {
+ $sub_ratio = ( $marker_distance - $prev_distance )
+ / ( $total_distance - $prev_distance );
+ }
+ return (
+ $prev->[1] + ( $this->[1] - $prev->[1] ) * $sub_ratio,
+ $prev->[0] + ( $this->[0] - $prev->[0] ) * $sub_ratio,
+ );
+ }
+ }
+ }
+ }
+ return (
+ $opt{from}[2]{lat} + ( $opt{to}[2]{lat} - $opt{from}[2]{lat} ) * $ratio,
+ $opt{from}[2]{lon} + ( $opt{to}[2]{lon} - $opt{from}[2]{lon} ) * $ratio
+ );
+}
+
+sub estimate_trip_position {
+ my ( $self, $in_transit ) = @_;
+
+ my @now_latlon;
+ my @route = @{ $in_transit->{route} };
+
+ # estimate_train_position runs before postprocess, so all route
+ # timestamps are provided in UNIX seconds and not as DateTime objects.
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' )->epoch;
+
+ my $prev_ts;
+ for my $i ( 0 .. $#route ) {
+ my $ts = $route[$i][2]{rt_arr} // $route[$i][2]{sched_arr}
+ // $route[$i][2]{rt_dep} // $route[$i][2]{sched_dep} // 0;
+ my $ts_dep = $route[$i][2]{rt_dep} // $route[$i][2]{sched_dep}
+ // $route[$i][2]{rt_arr} // $route[$i][2]{sched_arr} // 0;
+ if ( $ts and $ts_dep and $now >= $ts and $now <= $ts_dep ) {
+
+ # Currently at a stop
+ @now_latlon = ( $route[$i][2]{lat}, $route[$i][2]{lon} );
+ last;
+ }
+ if ( $ts
+ and $prev_ts
+ and $now > $prev_ts
+ and $now < $ts )
+ {
+ @now_latlon = $self->estimate_trip_position_between_stops(
+ now => $now,
+ from => $route[ $i - 1 ],
+ from_ts => $prev_ts,
+ to => $route[$i],
+ to_ts => $ts,
+ polyline => $in_transit->{polyline},
+ );
+ last;
+ }
+ $prev_ts = $ts_dep;
+ }
+
+ if ( not @now_latlon
+ and $in_transit->{sched_dep_ts}
+ and $in_transit->{sched_arr_ts} )
+ {
+ my $time_complete = $now
+ - ( $in_transit->{real_dep_ts} // $in_transit->{sched_dep_ts} );
+ my $time_total
+ = ( $in_transit->{real_arr_ts} // $in_transit->{sched_arr_ts} )
+ - ( $in_transit->{real_dep_ts} // $in_transit->{sched_dep_ts} );
+
+ if ( $time_total == 0 ) {
+ return [ $in_transit->{dep_lat}, $in_transit->{dep_lon} ];
+ }
+
+ my $completion = $time_complete / $time_total;
+ $completion = $completion < 0 ? 0 : $completion > 1 ? 1 : $completion;
+ @now_latlon = (
+ $in_transit->{dep_lat}
+ + ( $in_transit->{arr_lat} - $in_transit->{dep_lat} )
+ * $completion,
+ $in_transit->{dep_lon}
+ + ( $in_transit->{arr_lon} - $in_transit->{dep_lon} )
+ * $completion,
+ );
+ }
+
+ return \@now_latlon;
+}
+
1;
diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm
index 0fb663e..bce475f 100755
--- a/lib/Travelynx/Model/Journeys.pm
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -4,16 +4,16 @@ package Travelynx::Model::Journeys;
#
# SPDX-License-Identifier: AGPL-3.0-or-later
-use GIS::Distance;
-use List::MoreUtils qw(after_incl before_incl);
-
use strict;
use warnings;
use 5.020;
use utf8;
use DateTime;
+use DateTime::Format::Strptime;
+use GIS::Distance;
use JSON;
+use List::MoreUtils qw(after_incl before_incl first_index last_index);
my %visibility_itoa = (
100 => 'public',
@@ -50,6 +50,8 @@ sub epoch_to_dt {
);
}
+# TODO turn into a travelynx helper called from templates so that
+# loc_handle is available for localization
sub min_to_human {
my ( $self, $minutes ) = @_;
@@ -183,20 +185,44 @@ sub add {
}
if ( $opt{route} ) {
+ my $parser = DateTime::Format::Strptime->new(
+ pattern => '%d.%m.%Y %H:%M',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
my @unknown_stations;
+ my $prev_epoch = 0;
+
for my $station ( @{ $opt{route} } ) {
+ my $ts;
+ my %station_data;
+ if ( $station
+ =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x )
+ {
+ $station = $+{stop};
+ $ts = $parser->parse_datetime( $+{timestamp} );
+ if ($ts) {
+ my $epoch = $ts->epoch;
+ if ( $epoch < $prev_epoch ) {
+ return ( undef,
+'Zeitstempel der Unterwegshalte müssen monoton steigend sein (keine Zeitreisen und keine Portale)'
+ );
+ }
+ $station_data{sched_arr} = $epoch;
+ $station_data{sched_dep} = $epoch;
+ $prev_epoch = $epoch;
+ }
+ }
my $station_info = $self->{stations}
->search( $station, backend_id => $opt{backend_id} );
if ($station_info) {
+ $station_data{lat} = $station_info->{lat};
+ $station_data{lon} = $station_info->{lon};
push(
@route,
[
- $station_info->{name},
- $station_info->{eva},
- {
- lat => $station_info->{lat},
- lon => $station_info->{lon},
- }
+ $station_info->{name}, $station_info->{eva},
+ \%station_data,
]
);
}
@@ -283,8 +309,14 @@ sub add_from_in_transit {
my $db = $opt{db};
my $journey = $opt{journey};
+ if ( $journey->{train_id} eq 'manual' ) {
+ $journey->{edited} = 0x3fff;
+ }
+ else {
+ $journey->{edited} = 0;
+ }
+
delete $journey->{data};
- $journey->{edited} = 0;
$journey->{checkout_time} = DateTime->now( time_zone => 'Europe/Berlin' );
return $db->insert( 'journeys', $journey, { returning => 'id' } )
@@ -301,16 +333,16 @@ sub update {
my $rows;
my $journey = $self->get_single(
- uid => $uid,
- db => $db,
- journey_id => $journey_id,
- with_datetime => 1,
- with_route_datetime => 1,
+ uid => $uid,
+ db => $db,
+ journey_id => $journey_id,
+ with_datetime => 1,
);
eval {
if ( exists $opt{from_name} ) {
- my $from_station = $self->{stations}->search( $opt{from_name} );
+ my $from_station = $self->{stations}
+ ->search( $opt{from_name}, backend_id => $journey->{backend_id} );
if ( not $from_station ) {
die("Unbekannter Startbahnhof\n");
}
@@ -326,7 +358,8 @@ sub update {
)->rows;
}
if ( exists $opt{to_name} ) {
- my $to_station = $self->{stations}->search( $opt{to_name} );
+ my $to_station = $self->{stations}
+ ->search( $opt{to_name}, backend_id => $journey->{backend_id} );
if ( not $to_station ) {
die("Unbekannter Zielbahnhof\n");
}
@@ -399,7 +432,40 @@ sub update {
)->rows;
}
if ( exists $opt{route} ) {
- my @new_route = map { [ $_, undef, {} ] } @{ $opt{route} };
+
+ # If $opt{route} is a subset of $journey->{route}, we can recycle all data
+ my @new_route;
+ my $new_route_i = 0;
+ for my $old_route_i ( 0 .. $#{ $journey->{route} } ) {
+ if ( $journey->{route}[$old_route_i][0] eq
+ $opt{route}[$new_route_i] )
+ {
+ $new_route_i += 1;
+ push( @new_route, $journey->{route}[$old_route_i] );
+ }
+ }
+
+ # Otherwise, fetch stop IDs so that polylines remain usable
+ if ( @new_route != @{ $opt{route} } ) {
+ my %stop
+ = map { $_->{name} => $_ } $self->{stations}->get_by_names(
+ backend_id => $journey->{backend_id},
+ names => [ $opt{route} ]
+ );
+ @new_route = map {
+ [
+ $_,
+ $stop{$_}{eva},
+ defined $stop{$_}{eva}
+ ? {
+ lat => $stop{$_}{lat},
+ lon => $stop{$_}{lon}
+ }
+ : {}
+ ]
+ } @{ $opt{route} };
+ }
+
$rows = $db->update(
'journeys',
{
@@ -537,6 +603,83 @@ sub pop {
return $journey;
}
+sub set_polyline {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $polyline = $opt{polyline};
+
+ my $from_eva = $opt{from_eva};
+ my $to_eva = $opt{to_eva};
+
+ my $polyline_str = JSON->new->encode($polyline);
+
+ my $pl_res = $db->select(
+ 'polylines',
+ ['id'],
+ {
+ origin_eva => $from_eva,
+ destination_eva => $to_eva,
+ polyline => $polyline_str,
+ },
+ { limit => 1 }
+ );
+
+ my $polyline_id;
+ if ( my $h = $pl_res->hash ) {
+ $polyline_id = $h->{id};
+ }
+ else {
+ $polyline_id = $db->insert(
+ 'polylines',
+ {
+ origin_eva => $from_eva,
+ destination_eva => $to_eva,
+ polyline => $polyline_str
+ },
+ { returning => 'id' }
+ )->hash->{id};
+ }
+ if ($polyline_id) {
+ $self->set_polyline_id(
+ uid => $uid,
+ db => $db,
+ polyline_id => $polyline_id,
+ journey_id => $opt{journey_id},
+ edited => $opt{edited},
+ );
+ $self->stats_cache->invalidate(
+ ts => epoch_to_dt( $opt{stats_ts} ),
+ db => $db,
+ uid => $uid
+ );
+ }
+
+}
+
+sub set_polyline_id {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $polyline_id = $opt{polyline_id};
+ my $journey_id = $opt{journey_id};
+ my $edited = $opt{edited};
+
+ $db->update(
+ 'journeys',
+ {
+ polyline_id => $polyline_id,
+ edited => $edited | 0x0040
+ },
+ {
+ user_id => $uid,
+ id => $opt{journey_id}
+ }
+ );
+}
+
sub get {
my ( $self, %opt ) = @_;
@@ -549,7 +692,7 @@ sub get {
my @select
= (
- 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)
+ 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_platform dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_platform arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility)
);
my %where = (
user_id => $uid,
@@ -613,12 +756,13 @@ sub get {
is_motis => $entry->{is_motis},
backend_name => $entry->{backend_name},
backend_id => $entry->{backend_id},
- type => $entry->{train_type},
+ type => $entry->{train_type} =~ s{ \s+ $ }{}rx,
line => $entry->{train_line},
no => $entry->{train_no},
from_eva => $entry->{dep_eva},
from_ds100 => $entry->{dep_ds100},
from_name => $entry->{dep_name},
+ from_platform => $entry->{dep_platform},
from_latlon => [ $entry->{dep_lat}, $entry->{dep_lon} ],
checkin_ts => $entry->{checkin_ts},
sched_dep_ts => $entry->{sched_dep_ts},
@@ -626,6 +770,7 @@ sub get {
to_eva => $entry->{arr_eva},
to_ds100 => $entry->{arr_ds100},
to_name => $entry->{arr_name},
+ to_platform => $entry->{arr_platform},
to_latlon => [ $entry->{arr_lat}, $entry->{arr_lon} ],
checkout_ts => $entry->{checkout_ts},
sched_arr_ts => $entry->{sched_arr_ts},
@@ -659,12 +804,18 @@ sub get {
$ref->{checkout} = epoch_to_dt( $ref->{checkout_ts} );
$ref->{sched_arrival} = epoch_to_dt( $ref->{sched_arr_ts} );
$ref->{rt_arrival} = epoch_to_dt( $ref->{rt_arr_ts} );
+ if ( $ref->{rt_dep_ts} and $ref->{sched_dep_ts} ) {
+ $ref->{delay_dep} = $ref->{rt_dep_ts} - $ref->{sched_dep_ts};
+ }
+ if ( $ref->{rt_arr_ts} and $ref->{sched_arr_ts} ) {
+ $ref->{delay_arr} = $ref->{rt_arr_ts} - $ref->{sched_arr_ts};
+ }
}
if ( $opt{with_route_datetime} ) {
for my $stop ( @{ $ref->{route} } ) {
for my $k (qw(rt_arr rt_dep sched_arr sched_dep)) {
if ( $stop->[2]{$k} ) {
- $stop->[2]{$k} = epoch_to_dt( $stop->[2]{$k} );
+ $stop->[2]{"${k}_dt"} = epoch_to_dt( $stop->[2]{$k} );
}
}
}
@@ -1023,6 +1174,8 @@ sub generate_missing_stats {
my $stats_index = 0;
+ my %need_year;
+
for my $journey_index ( 0 .. $#journey_months ) {
if ( $stats_index < @stats_months
and $journey_months[$journey_index][0]
@@ -1034,6 +1187,7 @@ sub generate_missing_stats {
}
else {
my ( $year, $month ) = @{ $journey_months[$journey_index] };
+ $need_year{$year} = 1;
$self->get_stats(
uid => $uid,
db => $db,
@@ -1043,6 +1197,14 @@ sub generate_missing_stats {
);
}
}
+ for my $year ( keys %need_year ) {
+ $self->get_stats(
+ uid => $uid,
+ db => $db,
+ year => $year,
+ write_only => 1
+ );
+ }
}
sub get_nav_months {
@@ -1133,9 +1295,10 @@ sub sanity_check {
. ' Stimmt das wirklich?';
}
if ( $journey->{edited} & 0x0010 and not $lax ) {
- my @unknown_stations
- = $self->{stations}
- ->grep_unknown( map { $_->[0] } @{ $journey->{route} } );
+ my @unknown_stations = $self->{stations}->grep_unknown(
+ backend_id => $journey->{backend_id},
+ names => [ map { $_->[0] } @{ $journey->{route} } ]
+ );
if (@unknown_stations) {
return 'Unbekannte Station(en): ' . join( ', ', @unknown_stations );
}
@@ -1150,9 +1313,11 @@ sub get_travel_distance {
my $from = $journey->{from_name};
my $from_eva = $journey->{from_eva};
my $from_latlon = $journey->{from_latlon};
+ my $from_ts = $journey->{sched_dep_ts} // $journey->{rt_dep_ts};
my $to = $journey->{to_name};
my $to_eva = $journey->{to_eva};
my $to_latlon = $journey->{to_latlon};
+ my $to_ts = $journey->{sched_arr_ts} // $journey->{rt_arr_ts};
my $route_ref = $journey->{route};
my $polyline_ref = $journey->{polyline};
@@ -1206,31 +1371,85 @@ sub get_travel_distance {
my $geo = GIS::Distance->new();
my $distance_beeline
= $geo->distance_metal( @{$from_latlon}, @{$to_latlon} );
- my @route
- = after_incl { ( $_->[1] and $_->[1] == $from_eva ) or $_->[0] eq $from }
+
+ # A trip may pass the same stop multiple times.
+ # Thus, two criteria must be met to select the start/end of the actual route:
+ # * stop name or ID matches, and
+ # * one of:
+ # - arrival/departure time at the stop matches, or
+ # - the stop does not have arrival/departure time
+ # In the latter case, we still face the risk of selecting the wrong
+ # start/end stop. However, we have no way of finding the right one. As the
+ # majority of trips do not pass the same stop multiple times, it's better
+ # to risk having a few inaccurate distances than not calculating the
+ # distance for any journey that lacks sched_dep/rt_dep or
+ # sched_from/rt_from.
+
+ my $route_start = first_index {
+ (
+ ( $_->[1] and $_->[1] == $from_eva or $_->[0] eq $from )
+ and ( not( defined $_->[2]{sched_dep} or defined $_->[2]{rt_dep} )
+ or ( $_->[2]{sched_dep} // $_->[2]{rt_dep} ) == $from_ts )
+ )
+ }
@{$route_ref};
- @route
- = before_incl { ( $_->[1] and $_->[1] == $to_eva ) or $_->[0] eq $to }
- @route;
- if (
- @route < 2
- or ( $route[-1][0] ne $to
- and ( not $route[-1][1] or $route[-1][1] != $to_eva ) )
- )
- {
+ # Here, we need to use last_index. In case of ring lines, the first index
+ # will not have sched_arr/rt_arr set, but we should not select it as route
+ # end...
+ my $route_end = last_index {
+ (
+ ( $_->[1] and $_->[1] == $to_eva or $_->[0] eq $to )
+ and ( not( defined $_->[2]{sched_arr} or defined $_->[2]{rt_arr} )
+ or ( $_->[2]{sched_arr} // $_->[2]{rt_arr} ) == $to_ts )
+ )
+ }
+ @{$route_ref};
- # I AM ERROR
+ if ( not defined $route_start and defined $route_end ) {
return ( 0, 0, $distance_beeline );
}
- my @polyline = after_incl { $_->[2] and $_->[2] == $from_eva }
+ my %seen;
+ for my $stop ( @{$route_ref} ) {
+ if ( not defined $stop->[1] ) {
+ return ( 0, 0, $distance_beeline );
+ }
+ $seen{ $stop->[1] } //= 1;
+ $stop->[2]{n} = $seen{ $stop->[1] };
+ $seen{ $stop->[1] } += 1;
+ }
+
+ # Assumption: polyline entries are always [lat, lon] or [lat, lon, stop ID]
+ %seen = ();
+ for my $entry ( @{ $polyline_ref // [] } ) {
+ if ( $entry->[2] ) {
+ $seen{ $entry->[2] } //= 1;
+ $entry->[3] = $seen{ $entry->[2] };
+ $seen{ $entry->[2] } += 1;
+ }
+ }
+
+ $journey->{route_dep_index} = $route_start;
+ $journey->{route_arr_index} = $route_end;
+
+ my @route = @{$route_ref}[ $route_start .. $route_end ];
+
+ # Just like the route, the polyline may contain the same stop more than
+ # once. So we need to select based on the seen counter.
+ my $poly_start = first_index {
+ $_->[2] and $_->[2] == $from_eva and $_->[3] == $route[0][2]{n}
+ }
+ @{ $polyline_ref // [] };
+ my $poly_end = first_index {
+ $_->[2] and $_->[2] == $to_eva and $_->[3] == $route[-1][2]{n}
+ }
@{ $polyline_ref // [] };
- @polyline
- = before_incl { $_->[2] and $_->[2] == $to_eva } @polyline;
- # ensure that before_incl matched -- otherwise, @polyline is too long
- if ( @polyline and $polyline[-1][2] == $to_eva ) {
+ if ( defined $poly_start and defined $poly_end ) {
+ $journey->{poly_dep_index} = $poly_start;
+ $journey->{poly_arr_index} = $poly_end;
+ my @polyline = @{$polyline_ref}[ $poly_start .. $poly_end ];
my $prev_station = shift @polyline;
for my $station (@polyline) {
$distance_polyline += $geo->distance_metal(
@@ -1850,6 +2069,34 @@ sub get_latest_dest_ids {
);
}
+sub get_frequent_backend_ids {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $threshold = $opt{threshold}
+ // DateTime->now( time_zone => 'Europe/Berlin' )->subtract( months => 4 );
+ my $limit = $opt{limit} // 5;
+ my $db = $opt{db} //= $self->{pg}->db;
+
+ my $res = $db->select(
+ 'journeys',
+ 'count(*) as count, backend_id',
+ {
+ user_id => $uid,
+ real_departure => { '>', $threshold },
+ },
+ {
+ group_by => ['backend_id'],
+ order_by => { -desc => 'count' },
+ limit => $limit,
+ }
+ );
+
+ my @backend_ids = $res->hashes->map( sub { shift->{backend_id} } )->each;
+
+ return @backend_ids;
+}
+
# Returns a listref of {eva, name} hashrefs for the specified backend.
sub get_connection_targets {
my ( $self, %opt ) = @_;
@@ -1857,9 +2104,14 @@ sub get_connection_targets {
my $uid = $opt{uid};
my $threshold = $opt{threshold}
// DateTime->now( time_zone => 'Europe/Berlin' )->subtract( months => 4 );
- my $db = $opt{db} //= $self->{pg}->db;
- my $min_count = $opt{min_count} // 3;
- my $dest_id = $opt{eva};
+ my $db = $opt{db} //= $self->{pg}->db;
+ my $min_count = $opt{min_count} // 3;
+ my $backend_id = $opt{backend_id};
+ my $dest_id = $opt{eva};
+
+ $self->{log}->debug(
+"get_connection_targets(uid => $uid, backend_id => $backend_id, dest_id => $dest_id)"
+ );
if ( $opt{destination_name} ) {
return {
@@ -1868,8 +2120,6 @@ sub get_connection_targets {
};
}
- my $backend_id = $opt{backend_id};
-
if ( not $dest_id ) {
( $dest_id, $backend_id ) = $self->get_latest_dest_ids(%opt);
}
@@ -1900,10 +2150,15 @@ sub get_connection_targets {
order_by => { -desc => 'count' }
}
);
- my @destinations
- = $res->hashes->grep( sub { shift->{count} >= $min_count } )
- ->map( sub { shift->{dest} } )
- ->each;
+ my @all_destinations = $res->hashes->each;
+ my @destinations;
+
+ while ( not @destinations and $min_count > 0 ) {
+ @destinations = map { $_->{dest} }
+ grep { $_->{count} >= $min_count } @all_destinations;
+ $min_count--;
+ }
+
@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 bf35d1a..6c647ec 100644
--- a/lib/Travelynx/Model/Stations.pm
+++ b/lib/Travelynx/Model/Stations.pm
@@ -205,6 +205,9 @@ sub add_or_update {
);
return;
}
+ if (not $stop->latlon) {
+ die('Backend Error: Stop "' . $stop->full_name . '" has no geo coordinates');
+ }
$opt{db}->insert(
'stations',
{
@@ -458,11 +461,16 @@ sub get_by_name {
# Slow
sub get_by_names {
- my ( $self, @names ) = @_;
+ my ( $self, %opt ) = @_;
- my @ret
- = $self->{pg}->db->select( 'stations', '*', { name => { '=', \@names } } )
- ->hashes->each;
+ my @ret = $self->{pg}->db->select(
+ 'stations',
+ '*',
+ {
+ name => { '=', $opt{names} },
+ source => $opt{backend_id}
+ }
+ )->hashes->each;
return @ret;
}
@@ -503,12 +511,27 @@ sub search {
# Slow
sub grep_unknown {
- my ( $self, @stations ) = @_;
+ my ( $self, %opt ) = @_;
- my %station = map { $_->{name} => 1 } $self->get_by_names(@stations);
- my @unknown_stations = grep { not $station{$_} } @stations;
+ my %station = map { $_->{name} => 1 } $self->get_by_names(%opt);
+ my @unknown_stations = grep { not $station{$_} } @{ $opt{names} };
return @unknown_stations;
}
+sub get_bahn_stationinfo {
+ my ( $self, %opt ) = @_;
+ $opt{db} //= $self->{pg}->db;
+
+ my $res
+ = $opt{db}
+ ->select( 'bahn_platform_directions', ['data'], { eva => $opt{eva} } )
+ ->expand->hash;
+
+ if ($res) {
+ return $res->{data};
+ }
+ return;
+}
+
1;
diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm
index be9e80b..3ef7f33 100644
--- a/lib/Travelynx/Model/Users.pm
+++ b/lib/Travelynx/Model/Users.pm
@@ -216,6 +216,14 @@ sub set_backend {
);
}
+sub set_language {
+ my ( $self, %opt ) = @_;
+ $opt{db} //= $self->{pg}->db;
+
+ $opt{db}
+ ->update( 'users', { language => $opt{language} }, { id => $opt{uid} } );
+}
+
sub set_privacy {
my ( $self, %opt ) = @_;
my $db = $opt{db} // $self->{pg}->db;
@@ -413,7 +421,7 @@ sub get {
my $user = $db->select(
'users_with_backend',
- 'id, name, status, public_level, email, '
+ 'id, name, status, public_level, email, language, '
. 'accept_follows, notifications, '
. 'extract(epoch from registered_at) as registered_at_ts, '
. 'extract(epoch from last_seen) as last_seen_ts, '
@@ -423,10 +431,11 @@ sub get {
)->hash;
if ($user) {
return {
- id => $user->{id},
- name => $user->{name},
- status => $user->{status},
- notifications => $user->{notifications},
+ id => $user->{id},
+ name => $user->{name},
+ languages => [ split( qr{[|]}, $user->{language} // q{} ) ],
+ status => $user->{status},
+ notifications => $user->{notifications},
accept_follows => $user->{accept_follows} == 2 ? 1 : 0,
accept_follow_requests => $user->{accept_follows} == 1 ? 1 : 0,
default_visibility => $user->{public_level} & 0x7f,