summaryrefslogtreecommitdiff
path: root/lib/Travelynx/Controller
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx/Controller')
-rw-r--r--lib/Travelynx/Controller/Account.pm189
-rwxr-xr-xlib/Travelynx/Controller/Api.pm20
-rwxr-xr-xlib/Travelynx/Controller/Profile.pm46
-rw-r--r--lib/Travelynx/Controller/Traewelling.pm2
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm1213
5 files changed, 1367 insertions, 103 deletions
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm
index faaad0a..b0722f7 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -1,6 +1,7 @@
package Travelynx::Controller::Account;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
@@ -344,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',
@@ -873,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) = @_;
@@ -1025,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;
@@ -1066,11 +1097,84 @@ sub backend_form {
if ( $backend->{iris} ) {
$type = 'IRIS-TTS';
$backend->{name} = 'IRIS';
- $backend->{longname} = 'Deutsche Bahn (IRIS-TTS)';
+ $backend->{longname} = 'Deutsche Bahn: IRIS-TTS';
$backend->{homepage} = 'https://www.bahn.de';
+ $backend->{legacy} = 1;
+ }
+ elsif ( $backend->{dbris} ) {
+ $type = 'DBRIS';
+ $backend->{longname} = 'Deutsche Bahn: bahn.de';
+ $backend->{homepage} = 'https://www.bahn.de';
+ $backend->{recommended} = 1;
+ }
+ elsif ( $backend->{efa} ) {
+ if ( my $s = $self->efa->get_service( $backend->{name} ) ) {
+ $type = 'EFA';
+ $backend->{longname} = $s->{name};
+ $backend->{homepage} = $s->{homepage};
+ $backend->{regions} = [ map { $place_map{$_} // $_ }
+ @{ $s->{coverage}{regions} // [] } ];
+ $backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
+ $backend->{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 ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ }
+ elsif ( $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'MultiPolygon'
+ and defined $user_lon )
+ {
+ for my $s_poly (
+ @{ $s->{coverage}{area}{coordinates} // [] } )
+ {
+ if (
+ $self->lonlat_in_polygon(
+ $s_poly, [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ last;
+ }
+ }
+ }
+ }
+ else {
+ $type = undef;
+ }
}
elsif ( $backend->{hafas} ) {
- if ( my $s = $self->hafas->get_service( $backend->{name} ) ) {
+
+ # These backends lack a journey endpoint or are no longer
+ # operational and are thus useless for travelynx
+ if ( $backend->{name} eq 'Resrobot'
+ or $backend->{name} eq 'TPG'
+ or $backend->{name} eq 'VRN'
+ or $backend->{name} eq 'DB' )
+ {
+ $type = undef;
+ }
+
+ # PKP is behind a GeoIP filter. Only list it if travelynx.conf
+ # indicates that our IP is allowed or provides a proxy.
+ elsif (
+ $backend->{name} eq 'PKP'
+ and not( $self->app->config->{hafas}{PKP}{geoip_ok}
+ or $self->app->config->{hafas}{PKP}{proxy} )
+ )
+ {
+ $type = undef;
+ }
+ elsif ( my $s = $self->hafas->get_service( $backend->{name} ) ) {
$type = 'HAFAS';
$backend->{longname} = $s->{name};
$backend->{homepage} = $s->{homepage};
@@ -1078,9 +1182,17 @@ sub backend_form {
@{ $s->{coverage}{regions} // [] } ];
$backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
+ if ( $backend->{name} eq 'ÖBB' ) {
+ $backend->{recommended} = 1;
+ }
+ else {
+ $backend->{association} = 1;
+ }
+
if (
$s->{coverage}{area}
and $s->{coverage}{area}{type} eq 'Polygon'
+ and defined $user_lon
and $self->lonlat_in_polygon(
$s->{coverage}{area}{coordinates},
[ $user_lon, $user_lat ]
@@ -1090,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} // [] } )
@@ -1111,23 +1224,71 @@ sub backend_form {
$type = undef;
}
}
- $backend->{type} = $type;
- }
+ elsif ( $backend->{motis} ) {
+ my $s = $self->motis->get_service( $backend->{name} );
+
+ $type = 'MOTIS';
+ $backend->{longname} = $s->{name};
+ $backend->{homepage} = $s->{homepage};
+ $backend->{regions} = [ map { $place_map{$_} // $_ }
+ @{ $s->{coverage}{regions} // [] } ];
+ $backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
+ $backend->{experimental} = 1;
+
+ if ( $backend->{name} eq 'transitous' ) {
+ $backend->{regions} = ['Weltweit'];
+ }
+ if ( $backend->{name} eq 'RNV' ) {
+ $backend->{homepage} = 'https://rnv-online.de/';
+ }
- # These backends lack a journey endpoint and are useless for travelynx
- @backends
- = grep { $_->{name} ne 'Resrobot' and $_->{name} ne 'TPG' } @backends;
+ 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 ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ }
+ elsif ( $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'MultiPolygon'
+ and defined $user_lon )
+ {
+ for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } )
+ {
+ if (
+ $self->lonlat_in_polygon(
+ $s_poly, [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ last;
+ }
+ }
+ }
+ }
+ $backend->{type} = $type;
- my $iris = shift @backends;
+ $backend_by_id{ $backend->{id} } = $backend;
+ }
- @backends
- = sort { $a->{name} cmp $b->{name} } grep { $_->{type} } @backends;
+ my @frequent_backends = grep { $_->{type} }
+ map { $backend_by_id{$_} }
+ $self->journeys->get_frequent_backend_ids( uid => $user->{id} );
- unshift( @backends, $iris );
+ @backends = map { $_->[1] }
+ sort { $a->[0] cmp $b->[0] }
+ map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends;
$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 f31195a..fa40e76 100755
--- a/lib/Travelynx/Controller/Api.pm
+++ b/lib/Travelynx/Controller/Api.pm
@@ -21,6 +21,9 @@ sub sanitize {
if ( not defined $value ) {
return undef;
}
+ if ( not defined $type ) {
+ return $value ? ( '' . $value ) : undef;
+ }
if ( $type eq '' ) {
return '' . $value;
}
@@ -184,8 +187,16 @@ sub travel_v1 {
my $from_station = sanitize( q{}, $payload->{fromStation} );
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} );
- $hafas //= exists $payload->{train}{journeyID} ? 'DB' : undef;
+ my $motis = sanitize( undef, $payload->{motis} );
+
+ if ( not( $efa or $hafas or $motis )
+ and exists $payload->{train}{journeyID} )
+ {
+ $dbris //= 'bahn.de';
+ }
if (
not(
@@ -208,7 +219,7 @@ sub travel_v1 {
return;
}
- if ( not $hafas
+ if ( not( $dbris or $efa or $hafas or $motis )
and not $self->stations->search( $from_station, backend_id => 1 ) )
{
$self->render(
@@ -224,7 +235,7 @@ sub travel_v1 {
}
if ( $to_station
- and not $hafas
+ and not( $dbris or $efa or $hafas or $motis )
and not $self->stations->search( $to_station, backend_id => 1 ) )
{
$self->render(
@@ -287,7 +298,10 @@ sub travel_v1 {
station => $from_station,
train_id => $train_id,
uid => $uid,
+ dbris => $dbris,
+ efa => $efa,
hafas => $hafas,
+ motis => $motis,
);
}
)->then(
diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm
index c35642d..8f3a1b2 100755
--- a/lib/Travelynx/Controller/Profile.pm
+++ b/lib/Travelynx/Controller/Profile.pm
@@ -111,6 +111,14 @@ sub profile {
$status->{arr_name} = undef;
}
+ my $map_data = {};
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ with_now_markers => 1,
+ );
+ }
+
my @journeys;
if (
@@ -190,6 +198,8 @@ sub profile {
: 0,
journey => $status,
journeys => [@journeys],
+ with_map => 1,
+ %{$map_data},
}
);
}
@@ -359,6 +369,21 @@ sub user_status {
return;
}
+ if ( not $ts =~ m{ ^ \d+ [.]? \d* $ }x ) {
+ $self->respond_to(
+ json => {
+ json => { error => 'bad request (invalid timestamp)' },
+ status => 400,
+ },
+ any => {
+ template => 'bad_request',
+ message => 'Invalid timestamp',
+ status => 400
+ }
+ );
+ return;
+ }
+
my $my_user;
my $relation;
my $inverse_relation;
@@ -494,6 +519,14 @@ sub user_status {
$og_data{description} = $tw_data{description} = q{};
}
+ my $map_data = {};
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ with_now_markers => 1,
+ );
+ }
+
$self->respond_to(
json => {
json => {
@@ -516,7 +549,9 @@ sub user_status {
journey => $status,
twitter => \%tw_data,
opengraph => \%og_data,
- version => $self->app->config->{version} // 'UNKNOWN',
+ with_map => 1,
+ %{$map_data},
+ version => $self->app->config->{version} // 'UNKNOWN',
},
);
}
@@ -555,6 +590,7 @@ sub status_card {
my $status = $self->get_user_status( $user->{id} );
my $visibility;
+ my $map_data = {};
if ( $status->{checked_in} or $status->{arr_name} ) {
my $visibility = $status->{effective_visibility};
if (
@@ -579,12 +615,20 @@ sub status_card {
$status->{arr_name} = undef;
}
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ with_now_markers => 1,
+ );
+ }
+
$self->render(
'_public_status_card',
name => $name,
privacy => $user,
journey => $status,
from_profile => $self->param('profile') ? 1 : 0,
+ %{$map_data},
);
}
diff --git a/lib/Travelynx/Controller/Traewelling.pm b/lib/Travelynx/Controller/Traewelling.pm
index 3cdeff8..6aa789c 100644
--- a/lib/Travelynx/Controller/Traewelling.pm
+++ b/lib/Travelynx/Controller/Traewelling.pm
@@ -29,7 +29,7 @@ sub oauth {
redirect_uri =>
$self->base_url_for('/oauth/traewelling')->to_abs->scheme(
$self->app->mode eq 'development' ? 'http' : 'https'
- )->to_string,
+ )->to_string,
scope => 'read-statuses write-statuses'
}
)->then(
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index f61d83d..5595e3c 100755
--- a/lib/Travelynx/Controller/Traveling.pm
+++ b/lib/Travelynx/Controller/Traveling.pm
@@ -1,18 +1,22 @@
package Travelynx::Controller::Traveling;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
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
@@ -41,6 +45,23 @@ sub get_connecting_trains_p {
my $promise = Mojo::Promise->new;
+ if ( $user->{backend_dbris} ) {
+
+ # We do get a little bit of via information, so this might work in some
+ # cases. But not reliably. Probably best to leave it out entirely then.
+ return $promise->reject;
+ }
+ if ( $user->{backend_efa} ) {
+
+ # TODO
+ return $promise->reject;
+ }
+ if ( $user->{backend_motis} ) {
+
+ # FIXME: The following code can't handle external_ids currently
+ return $promise->reject;
+ }
+
if ( $opt{eva} ) {
if ( $use_history & 0x01 ) {
$eva = $opt{eva};
@@ -105,6 +126,8 @@ sub get_connecting_trains_p {
my $iris_promise = Mojo::Promise->new;
my %via_count = map { $_->{name} => 0 } @destinations;
+ my $backend
+ = $self->stations->get_backend( backend_id => $opt{backend_id} );
if ( $opt{backend_id} == 0 ) {
$self->iris->get_departures_p(
station => $eva,
@@ -259,9 +282,14 @@ sub get_connecting_trains_p {
}
)->wait;
}
- else {
- my $hafas_service
- = $self->stations->get_hafas_name( backend_id => $opt{backend_id} );
+ elsif ( $backend->{dbris} ) {
+ return $promise->reject;
+ }
+ elsif ( $backend->{efa} ) {
+ return $promise->reject;
+ }
+ elsif ( $backend->{hafas} ) {
+ my $hafas_service = $backend->{name};
$self->hafas->get_departures_p(
service => $hafas_service,
eva => $eva,
@@ -341,6 +369,14 @@ sub homepage {
$self->stash( timeline => [@timeline] );
my @recent_targets;
if ( $status->{checked_in} ) {
+ my $map_data = {};
+ if ( $status->{arr_name} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ show_full_route => 1,
+ with_now_markers => 1,
+ );
+ }
my $journey_visibility
= $self->compute_effective_visibility(
$user->{default_visibility_str},
@@ -359,6 +395,8 @@ sub homepage {
journey_visibility => $journey_visibility,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
}
@@ -369,6 +407,8 @@ sub homepage {
user => $user,
user_status => $status,
journey_visibility => $journey_visibility,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
}
@@ -381,13 +421,15 @@ sub homepage {
user => $user,
user_status => $status,
journey_visibility => $journey_visibility,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
return;
}
}
else {
- @recent_targets = uniq_by { $_->{eva} }
+ @recent_targets = uniq_by { $_->{external_id_or_eva} }
$self->journeys->get_latest_checkout_stations( uid => $uid );
}
$self->render(
@@ -419,6 +461,14 @@ sub status_card {
$self->stash( timeline => [@timeline] );
if ( $status->{checked_in} ) {
+ my $map_data = {};
+ if ( $status->{arr_name} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ show_full_route => 1,
+ with_now_markers => 1,
+ );
+ }
my $journey_visibility
= $self->compute_effective_visibility(
$self->current_user->{default_visibility_str},
@@ -436,6 +486,7 @@ sub status_card {
journey_visibility => $journey_visibility,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
+ %{$map_data},
);
}
)->catch(
@@ -444,6 +495,7 @@ sub status_card {
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
+ %{$map_data},
);
}
)->wait;
@@ -453,6 +505,7 @@ sub status_card {
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
+ %{$map_data},
);
}
elsif ( $status->{cancellation} ) {
@@ -523,15 +576,125 @@ sub geolocation {
return;
}
- my $hafas_service
- = $self->stations->get_hafas_name( backend_id => $backend_id );
+ my ( $dbris_service, $efa_service, $hafas_service, $motis_service );
+ my $backend = $self->stations->get_backend( backend_id => $backend_id );
+ if ( $backend->{dbris} ) {
+ $dbris_service = $backend->{name};
+ }
+ if ( $backend->{efa} ) {
+ $efa_service = $backend->{name};
+ }
+ elsif ( $backend->{hafas} ) {
+ $hafas_service = $backend->{name};
+ }
+ elsif ( $backend->{motis} ) {
+ $motis_service = $backend->{name};
+ }
+
+ if ($dbris_service) {
+ $self->render_later;
+
+ $self->dbris->geosearch_p(
+ latitude => $lat,
+ longitude => $lon
+ )->then(
+ sub {
+ my ($dbris) = @_;
+ my @results = map {
+ {
+ name => $_->name,
+ eva => $_->eva,
+ distance => 0,
+ dbris => $dbris_service,
+ }
+ } uniq_by { $_->name } $dbris->results;
+ if ( @results > 10 ) {
+ @results = @results[ 0 .. 9 ];
+ }
+ $self->render(
+ json => {
+ candidates => [@results],
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ 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;
+ return;
+ }
+ elsif ($efa_service) {
+ $self->render_later;
+
+ Travel::Status::DE::EFA->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ service => $efa_service,
+ coord => {
+ lat => $lat,
+ lon => $lon
+ }
+ )->then(
+ sub {
+ my ($efa) = @_;
+ my @results = map {
+ {
+ name => $_->full_name,
+ eva => $_->id_code,
+ distance => 0,
+ efa => $efa_service,
+ }
+ } $efa->results;
+ if ( @results > 10 ) {
+ @results = @results[ 0 .. 9 ];
+ }
+ $self->render(
+ json => {
+ candidates => [@results],
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ error => $err,
+ },
- if ($hafas_service) {
+ # See above
+ # status => 502
+ );
+ }
+ )->wait;
+ return;
+ }
+ elsif ($hafas_service) {
$self->render_later;
+ my $agent = $self->ua;
+ if ( my $proxy = $self->app->config->{hafas}{$hafas_service}{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
Travel::Status::DE::HAFAS->new_p(
promise => 'Mojo::Promise',
- user_agent => $self->ua,
+ user_agent => $agent,
service => $hafas_service,
geoSearch => {
lat => $lat,
@@ -563,10 +726,65 @@ sub geolocation {
$self->render(
json => {
candidates => [],
- warning => $err,
+ error => $err,
+ },
+
+ # See above
+ #status => 502
+ );
+ }
+ )->wait;
+
+ return;
+ }
+ elsif ($motis_service) {
+ $self->render_later;
+
+ Travel::Status::MOTIS->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => $self->ua,
+ time_zone => 'Europe/Berlin',
+
+ service => $motis_service,
+ stops_by_coordinate => {
+ lat => $lat,
+ lon => $lon
+ }
+ )->then(
+ sub {
+ my ($motis) = @_;
+ my @motis = map {
+ {
+ id => $_->id,
+ name => $_->name,
+ distance => 0,
+ motis => $motis_service,
+ }
+ } $motis->results;
+
+ if ( @motis > 10 ) {
+ @motis = @motis[ 0 .. 9 ];
+ }
+
+ $self->render(
+ json => {
+ candidates => [@motis],
}
);
}
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ error => $err,
+ },
+
+ # See above
+ #status => 502
+ );
+ }
)->wait;
return;
@@ -648,10 +866,14 @@ sub travel_action {
$promise->then(
sub {
return $self->checkin_p(
- hafas => $params->{hafas},
- station => $params->{station},
- train_id => $params->{train},
- ts => $params->{ts},
+ dbris => $params->{dbris},
+ efa => $params->{efa},
+ hafas => $params->{hafas},
+ motis => $params->{motis},
+ station => $params->{station},
+ train_id => $params->{train},
+ train_suffix => $params->{suffix},
+ ts => $params->{ts},
);
}
)->then(
@@ -679,7 +901,13 @@ sub travel_action {
my ( $still_checked_in, undef ) = @_;
if ( my $destination = $params->{dest} ) {
my $station_link = '/s/' . $destination;
- if ( $status->{is_hafas} ) {
+ if ( $status->{is_dbris} ) {
+ $station_link .= '?dbris=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_efa} ) {
+ $station_link .= '?efa=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_hafas} ) {
$station_link .= '?hafas=' . $status->{backend_name};
}
$self->render(
@@ -715,7 +943,13 @@ sub travel_action {
sub {
my ( $still_checked_in, $error ) = @_;
my $station_link = '/s/' . $params->{station};
- if ( $status->{is_hafas} ) {
+ if ( $status->{is_dbris} ) {
+ $station_link .= '?dbris=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_efa} ) {
+ $station_link .= '?efa=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_hafas} ) {
$station_link .= '?hafas=' . $status->{backend_name};
}
@@ -766,13 +1000,33 @@ sub travel_action {
else {
my $redir = '/';
if ( $status->{checked_in} or $status->{cancelled} ) {
- if ( $status->{is_hafas} ) {
+ if ( $status->{is_dbris} ) {
+ $redir
+ = '/s/'
+ . $status->{dep_eva}
+ . '?dbris='
+ . $status->{backend_name};
+ }
+ elsif ( $status->{is_efa} ) {
+ $redir
+ = '/s/'
+ . $status->{dep_eva} . '?efa='
+ . $status->{backend_name};
+ }
+ elsif ( $status->{is_hafas} ) {
$redir
= '/s/'
. $status->{dep_eva}
. '?hafas='
. $status->{backend_name};
}
+ elsif ( $status->{is_motis} ) {
+ $redir
+ = '/s/'
+ . $status->{dep_external_id}
+ . '?motis='
+ . $status->{backend_name};
+ }
else {
$redir = '/s/' . $status->{dep_ds100};
}
@@ -788,7 +1042,10 @@ sub travel_action {
elsif ( $params->{action} eq 'cancelled_from' ) {
$self->render_later;
$self->checkin_p(
+ dbris => $params->{dbris},
+ efa => $params->{efa},
hafas => $params->{hafas},
+ motis => $params->{motis},
station => $params->{station},
train_id => $params->{train},
ts => $params->{ts},
@@ -917,10 +1174,70 @@ sub station {
$timestamp = DateTime->now( time_zone => 'Europe/Berlin' );
}
- my $hafas_service = $self->param('hafas')
- // ( $user->{backend_hafas} ? $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 ($hafas_service) {
+ if ($dbris_service) {
+ if ( $station !~ m{ [@] L = \d+ }x ) {
+ $self->render_later;
+ $self->dbris->get_station_id_p($station)->then(
+ sub {
+ my ($dbris_station) = @_;
+ $self->redirect_to( '/s/' . $dbris_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->redirect_to('/');
+ }
+ )->wait;
+ return;
+ }
+ $promise = $self->dbris->get_departures_p(
+ station => $station,
+ timestamp => $timestamp,
+ lookbehind => 30,
+ );
+ }
+ elsif ($efa_service) {
+ $promise = $self->efa->get_departures_p(
+ service => $efa_service,
+ name => $station,
+ timestamp => $timestamp,
+ lookbehind => 10,
+ lookahead => 50,
+ );
+ }
+ elsif ($hafas_service) {
$promise = $self->hafas->get_departures_p(
service => $hafas_service,
eva => $station,
@@ -929,6 +1246,35 @@ sub station {
lookahead => 30,
);
}
+ elsif ($motis_service) {
+ if ( $station !~ m/.*_.*/ ) {
+ $self->render_later;
+ $self->motis->get_station_by_query_p(
+ service => $motis_service,
+ query => $station,
+ )->then(
+ sub {
+ my ($motis_station) = @_;
+ $self->redirect_to( '/s/' . $motis_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ say "$err";
+
+ $self->redirect_to('/');
+ }
+ )->wait;
+ return;
+ }
+ $promise = $self->motis->get_departures_p(
+ service => $motis_service,
+ station_id => $station,
+ timestamp => $timestamp,
+ lookbehind => 30,
+ lookahead => 30,
+ );
+ }
else {
$promise = $self->iris->get_departures_p(
station => $station,
@@ -946,7 +1292,53 @@ sub station {
my $now_within_range
= abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0;
- if ($hafas_service) {
+ if ($dbris_service) {
+
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->dep->epoch ] } $status->results;
+
+ $status = {
+ station_eva => $station,
+ related_stations => [],
+ };
+
+ if ( $station =~ m{ [@] O = (?<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) {
@results = map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
@@ -967,6 +1359,51 @@ sub station {
related_stations => [],
};
}
+ elsif ($efa_service) {
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->datetime->epoch ] } $status->results;
+ $status = {
+ station_eva => $status->stop->id_num,
+ station_name => $status->stop->full_name,
+ related_stations => [],
+ };
+ 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] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->stopover->departure->epoch ] }
+ $status->results;
+
+ $status = {
+ station_eva => $station,
+ station_name =>
+ $status->{results}->[0]->stopover->stop->name,
+ related_stations => [],
+ };
+ }
else {
# You can't check into a train which terminates here
@@ -982,10 +1419,12 @@ sub station {
my $user_status = $self->get_user_status;
my $can_check_out = 0;
+ my ($eva) = ( $station =~ m{ [@] L = (\d+) }x );
+ $eva //= $status->{station_eva};
if ( $user_status->{checked_in} ) {
for my $stop ( @{ $user_status->{route_after} } ) {
if (
- $stop->[1] eq $status->{station_eva}
+ $stop->[1] eq $eva
or List::Util::any { $stop->[1] eq $_->{uic} }
@{ $status->{related_stations} }
)
@@ -997,7 +1436,7 @@ sub station {
}
my $connections_p;
- if ( $trip_id and $hafas_service ) {
+ if ( $trip_id and ( $dbris_service or $hafas_service ) ) {
@results = grep { $_->id eq $trip_id } @results;
}
elsif ( $train and not $hafas_service ) {
@@ -1013,12 +1452,14 @@ sub station {
eva => $user_status->{cancellation}{dep_eva},
destination_name =>
$user_status->{cancellation}{arr_name},
+ efa => $efa_service,
hafas => $hafas_service,
);
}
else {
$connections_p = $self->get_connecting_trains_p(
eva => $status->{station_eva},
+ efa => $efa_service,
hafas => $hafas_service
);
}
@@ -1031,7 +1472,10 @@ sub station {
$self->render(
'departures',
user => $user,
+ dbris => $dbris_service,
+ efa => $efa_service,
hafas => $hafas_service,
+ motis => $motis_service,
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
@@ -1050,7 +1494,10 @@ sub station {
$self->render(
'departures',
user => $user,
+ dbris => $dbris_service,
+ efa => $efa_service,
hafas => $hafas_service,
+ motis => $motis_service,
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
@@ -1059,6 +1506,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}",
);
}
@@ -1068,7 +1516,10 @@ sub station {
$self->render(
'departures',
user => $user,
+ dbris => $dbris_service,
+ efa => $efa_service,
hafas => $hafas_service,
+ motis => $motis_service,
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
@@ -1077,6 +1528,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}",
);
}
@@ -1091,6 +1543,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' )
@@ -1131,18 +1596,23 @@ sub station {
}
)->wait;
}
- elsif ( $err =~ m{svcRes|connection close} ) {
+ elsif ( $err
+ =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden|HTTP 500 Internal Server Error|HTTP 429 Too Many Requests}
+ )
+ {
$self->render(
'bad_gateway',
- message => $err,
- status => 502
+ message => $err,
+ status => 502,
+ select_new_backend => 1,
);
}
elsif ( $err =~ m{timeout}i ) {
$self->render(
'gateway_timeout',
- message => $err,
- status => 504
+ message => $err,
+ status => 504,
+ select_new_backend => 1,
);
}
else {
@@ -1161,7 +1631,40 @@ sub redirect_to_station {
my ($self) = @_;
my $station = $self->param('station');
- $self->redirect_to("/s/${station}");
+ if ( $self->param('backend_dbris') ) {
+ $self->render_later;
+ $self->dbris->get_station_id_p($station)->then(
+ sub {
+ my ($dbris_station) = @_;
+ $self->redirect_to( '/s/' . $dbris_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->redirect_to('/');
+ }
+ )->wait;
+ }
+ elsif ( $self->param('backend_motis') ) {
+ $self->render_later;
+ $self->motis->get_station_by_query(
+ service => $self->param('backend_motis'),
+ query => $station,
+ )->then(
+ sub {
+ my ($motis_station) = @_;
+ $self->redirect_to( '/s/' . $motis_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->redirect_to('/');
+ }
+ )->wait;
+ }
+ else {
+ $self->redirect_to("/s/${station}");
+ }
}
sub cancelled {
@@ -1317,23 +1820,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,
@@ -1411,15 +1910,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;
@@ -1436,13 +1939,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}
)
@@ -1687,11 +2194,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;
}
@@ -1707,6 +2220,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,
@@ -1744,29 +2291,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 => {}
+ }
);
}
@@ -1946,7 +2503,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'
@@ -1957,7 +2519,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,
@@ -1978,7 +2541,7 @@ sub edit_journey {
uid => $uid,
db => $db,
id => $journey->{id},
- $key => $self->param($key)
+ $key => $self->param($key),
);
if ($error) {
last;
@@ -2049,8 +2612,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') );
+ }
}
}
@@ -2070,17 +2639,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'
);
@@ -2100,7 +2880,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;
}
@@ -2138,7 +2918,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);
@@ -2174,4 +2954,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;