summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rwxr-xr-xlib/Travelynx.pm8
-rw-r--r--lib/Travelynx/Controller/Account.pm7
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm212
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm102
4 files changed, 328 insertions, 1 deletions
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 607b153..d8e1cf4 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -69,6 +69,8 @@ sub startup {
$self->types->type( csv => 'text/csv; charset=utf-8' );
$self->types->type( json => 'application/json; charset=utf-8' );
+ $self->types->type( gpx => 'application/gpx+xml; charset=utf-8' );
+ $self->types->type( xml => 'text/xml; charset=utf-8' );
$self->plugin('Config');
@@ -3174,6 +3176,11 @@ sub startup {
$authed_r->get( '/journey/:id' => [ format => [ 'html', 'json' ] ] )
->to( 'traveling#journey_details', format => undef )
->name('journey');
+ $authed_r->get( '/polyline/:id' => [ format => [ 'gpx', 'json' ] ] )->to(
+ 'traveling#journey_details',
+ format => undef,
+ polyline_export => 1
+ )->name('polyline_download');
$authed_r->get('/s/*station')->to('traveling#station');
$authed_r->get('/confirm_mail/:token')->to('account#confirm_mail');
$authed_r->post('/account/privacy')->to('account#privacy');
@@ -3186,6 +3193,7 @@ sub startup {
$authed_r->post('/account/select_backend')->to('account#change_backend');
$authed_r->post('/checkin/add')->to('traveling#add_intransit_form');
$authed_r->post('/journey/add')->to('traveling#add_journey_form');
+ $authed_r->post('/polyline/set')->to('traveling#set_polyline');
$authed_r->post('/journey/comment')->to('traveling#comment_form');
$authed_r->post('/journey/visibility')->to('traveling#visibility_form');
$authed_r->post('/journey/edit')->to('traveling#edit_journey');
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm
index 3a1e281..96be200 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -1055,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;
@@ -1272,8 +1273,13 @@ sub backend_form {
}
}
$backend->{type} = $type;
+
+ $backend_by_id{ $backend->{id} } = $backend;
}
+ my @frequent_backends = 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;
@@ -1281,6 +1287,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/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index e23301e..bde2902 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
@@ -2149,6 +2151,32 @@ sub journey_details {
);
if ($journey) {
+
+ if ( $self->stash('polyline_export') ) {
+ 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,
@@ -2539,6 +2567,188 @@ sub edit_journey {
);
}
+sub polyline_add_stops {
+ my ( $self, %opt ) = @_;
+
+ my $polyline = $opt{polyline};
+ my $route = $opt{route};
+
+ my $distance = GIS::Distance->new;
+
+ my %min_dist;
+ for my $stop ( @{$route} ) {
+ for my $polyline_index ( 0 .. $#{$polyline} ) {
+ my $pl = $polyline->[$polyline_index];
+ my $dist
+ = $distance->distance_metal( $stop->[2]{lat}, $stop->[2]{lon},
+ $pl->[1], $pl->[0] );
+ if ( not $min_dist{ $stop->[1] }
+ or $min_dist{ $stop->[1] }{dist} > $dist )
+ {
+ $min_dist{ $stop->[1] } = {
+ dist => $dist,
+ index => $polyline_index,
+ };
+ }
+ }
+ }
+ for my $stop ( @{$route} ) {
+ if ( $min_dist{ $stop->[1] } ) {
+ if ( defined $polyline->[ $min_dist{ $stop->[1] }{index} ][2] ) {
+ return sprintf(
+ 'Error: Stop IDs %d and %d both map to lon %f, lat %f',
+ $polyline->[ $min_dist{ $stop->[1] }{index} ][2],
+ $stop->[1],
+ $polyline->[ $min_dist{ $stop->[1] }{index} ][0],
+ $polyline->[ $min_dist{ $stop->[1] }{index} ][1]
+ );
+ }
+ $polyline->[ $min_dist{ $stop->[1] }{index} ][2]
+ = $stop->[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],
+ );
+ }
+
+ $self->redirect_to("/journey/${journey_id}");
+}
+
sub add_journey_form {
my ($self) = @_;
diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm
index 9efa365..7621a5f 100755
--- a/lib/Travelynx/Model/Journeys.pm
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -569,6 +569,78 @@ 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},
+ );
+ }
+
+}
+
+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 ) = @_;
@@ -1943,6 +2015,36 @@ 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;
+
+ say join( ' ', @backend_ids );
+
+ return @backend_ids;
+}
+
# Returns a listref of {eva, name} hashrefs for the specified backend.
sub get_connection_targets {
my ( $self, %opt ) = @_;