diff options
Diffstat (limited to 'lib')
-rwxr-xr-x | lib/Travelynx.pm | 8 | ||||
-rw-r--r-- | lib/Travelynx/Controller/Account.pm | 7 | ||||
-rwxr-xr-x | lib/Travelynx/Controller/Traveling.pm | 212 | ||||
-rwxr-xr-x | lib/Travelynx/Model/Journeys.pm | 102 |
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 ) = @_; |