summaryrefslogtreecommitdiff
path: root/lib/Travelynx/Controller/Traveling.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx/Controller/Traveling.pm')
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm803
1 files changed, 720 insertions, 83 deletions
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index bb45378..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}",
);
}
@@ -1500,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(
@@ -1723,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,
@@ -1817,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;
@@ -1842,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}
)
@@ -2093,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;
}
@@ -2113,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,
@@ -2150,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 => {}
+ }
);
}
@@ -2352,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'
@@ -2363,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,
@@ -2384,7 +2539,7 @@ sub edit_journey {
uid => $uid,
db => $db,
id => $journey->{id},
- $key => $self->param($key)
+ $key => $self->param($key),
);
if ($error) {
last;
@@ -2455,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') );
+ }
}
}
@@ -2476,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'
);
@@ -2506,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;
}
@@ -2544,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);
@@ -2580,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;