summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cpanfile5
-rwxr-xr-xlib/Travelynx.pm1
-rw-r--r--lib/Travelynx/Controller/Account.pm3
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm187
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm77
-rw-r--r--share/locales/de_DE.po24
-rw-r--r--share/locales/en_GB.po13
-rw-r--r--templates/journey.html.ep77
8 files changed, 352 insertions, 35 deletions
diff --git a/cpanfile b/cpanfile
index 6fccfd1..221e919 100644
--- a/cpanfile
+++ b/cpanfile
@@ -8,6 +8,8 @@ requires 'GIS::Distance';
requires 'GIS::Distance::Fast';
requires 'IO::Socket::Socks', '>= 0.64';
requires 'IO::Socket::SSL', '>= 2.009';
+requires 'JSON';
+requires 'JSON::XS';
requires 'List::UtilsBy';
requires 'Locale::Maketext';
requires 'Locale::Maketext::Lexicon';
@@ -25,5 +27,4 @@ requires 'Travel::Status::DE::DBRIS', '>= 0.10';
requires 'Travel::Status::DE::HAFAS', '>= 6.20';
requires 'Travel::Status::DE::IRIS';
requires 'UUID::Tiny';
-requires 'JSON';
-requires 'JSON::XS';
+requires 'XML::LibXML';
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 586f1a0..d8e1cf4 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -3193,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 96be200..533e6eb 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -1277,7 +1277,8 @@ sub backend_form {
$backend_by_id{ $backend->{id} } = $backend;
}
- my @frequent_backends = map { $backend_by_id{$_} }
+ my @frequent_backends = grep { $_->{type} }
+ map { $backend_by_id{$_} }
$self->journeys->get_frequent_backend_ids( uid => $user->{id} );
@backends = map { $_->[1] }
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index 084b45e..4230163 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
@@ -2565,6 +2567,189 @@ 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],
+ stats_ts => $journey->{rt_dep_ts},
+ );
+ }
+
+ $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 8e83374..884f295 100755
--- a/lib/Travelynx/Model/Journeys.pm
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -569,6 +569,83 @@ sub pop {
return $journey;
}
+sub set_polyline {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $polyline = $opt{polyline};
+
+ my $from_eva = $opt{from_eva};
+ my $to_eva = $opt{to_eva};
+
+ my $polyline_str = JSON->new->encode($polyline);
+
+ my $pl_res = $db->select(
+ 'polylines',
+ ['id'],
+ {
+ origin_eva => $from_eva,
+ destination_eva => $to_eva,
+ polyline => $polyline_str,
+ },
+ { limit => 1 }
+ );
+
+ my $polyline_id;
+ if ( my $h = $pl_res->hash ) {
+ $polyline_id = $h->{id};
+ }
+ else {
+ $polyline_id = $db->insert(
+ 'polylines',
+ {
+ origin_eva => $from_eva,
+ destination_eva => $to_eva,
+ polyline => $polyline_str
+ },
+ { returning => 'id' }
+ )->hash->{id};
+ }
+ if ($polyline_id) {
+ $self->set_polyline_id(
+ uid => $uid,
+ db => $db,
+ polyline_id => $polyline_id,
+ journey_id => $opt{journey_id},
+ edited => $opt{edited},
+ );
+ $self->stats_cache->invalidate(
+ ts => epoch_to_dt( $opt{stats_ts} ),
+ db => $db,
+ uid => $uid
+ );
+ }
+
+}
+
+sub set_polyline_id {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $polyline_id = $opt{polyline_id};
+ my $journey_id = $opt{journey_id};
+ my $edited = $opt{edited};
+
+ $db->update(
+ 'journeys',
+ {
+ polyline_id => $polyline_id,
+ edited => $edited | 0x0040
+ },
+ {
+ user_id => $uid,
+ id => $opt{journey_id}
+ }
+ );
+}
+
sub get {
my ( $self, %opt ) = @_;
diff --git a/share/locales/de_DE.po b/share/locales/de_DE.po
index 97e3877..cf9be32 100644
--- a/share/locales/de_DE.po
+++ b/share/locales/de_DE.po
@@ -270,6 +270,30 @@ msgstr "Exportieren"
msgid "journey.edit"
msgstr "Bearbeiten"
+msgid "journey.map-data"
+msgstr "Kartendaten"
+
+msgid "journey.map.download"
+msgstr "Herunterladen"
+
+msgid "journey.map.upload"
+msgstr "Hochladen"
+
+msgid "journey.map.upload-full"
+msgstr "Komplette Route"
+
+msgid "journey.map.upload-partial"
+msgstr "Gefahrenes Segment"
+
+msgid "journey.map.info.download"
+msgstr "JSON-Format: [[lon, lat, ID], ...] in WGS84-Koordinaten. GPX-Dateien sind mit BRouter kompatibel."
+
+msgid "journey.map.info.upload"
+msgstr "GPX-Uploads müssen ein einzelnes track-Element mit einem einzelnen track segment enthalten. Ein BRouter-GPX-Export erfüllt diese Vorgaben. Uploads müssen entweder die komplette Route des Verkehrsmittels oder nur den zu diesem Checkin zugehörigen Abschnitt enthalten. Beim Hochladen bitte die passende Schaltfläche auswählen. IDs von Halten müssen beim Upload nicht angegeben werden. Bitte beachten: Beim Einspielen eigener Kartendaten werden die zuvor gespeicherten unwiderruflich gelöscht."
+
+msgid "journey.danger"
+msgstr "Danger Zone"
+
msgid "journey.delete"
msgstr "Löschen"
diff --git a/share/locales/en_GB.po b/share/locales/en_GB.po
index 48fb05a..7c6f97b 100644
--- a/share/locales/en_GB.po
+++ b/share/locales/en_GB.po
@@ -279,8 +279,17 @@ msgstr "Download"
msgid "journey.map.upload"
msgstr "Upload"
-msgid "journey.map.info"
-msgstr "JSON format: [[lon, lat, station ID], ...], with lon/lat in WGS84 coordinates. GPX files are compatible with BRouter. Uploads may cover either the entire route or just the route segment that belongs to this checkin; station IDs are optional when uploading map data."
+msgid "journey.map.upload-full"
+msgstr "Upload full route"
+
+msgid "journey.map.upload-partial"
+msgstr "Upload travelled segment"
+
+msgid "journey.map.info.download"
+msgstr "JSON format: [[lon, lat, station ID], ...], with lon/lat in WGS84 coordinates. GPX files are compatible with BRouter."
+
+msgid "journey.map.info.upload"
+msgstr "GPX uploads must contain a single track with a single track segment (such as provided by BRouter's export). They must cover either the full route or just the travelled route segment that belongs to this checkin. Please use the appropriate upload button, otherwise chaos may ensue. There is no need to specify station IDs when uploading tracks. Note that uploads irreversibly replace previously stored map data."
msgid "journey.danger"
msgstr "Danger Zone"
diff --git a/templates/journey.html.ep b/templates/journey.html.ep
index b7b4223..8a12ed5 100644
--- a/templates/journey.html.ep
+++ b/templates/journey.html.ep
@@ -350,40 +350,59 @@
% }
</div>
</div>
- <h2><%= L('journey.map-data') %></h2>
- <div class="row">
- <div class="col s12 m6 l6 center-align">
- <a class="btn waves-effect waves-light" href="<%= url_for('polyline_download', id => $journey->{id}, format => 'json' ) %>">
- <i class="material-icons left" aria-label="<%= L('journey.map.download') %>">file_download</i>
- JSON
- </a>
- </div>
- <div class="col s12 m6 l6 center-align">
- <a class="btn waves-effect waves-light" href="<%= url_for('polyline_download', id => $journey->{id}, format => 'gpx' ) %>">
- <i class="material-icons left" aria-label="<%= L('journey.map.download') %>">file_download</i>
- GPX
- </a>
- </div>
- </div>
- <div class="row">
- <div class="col s12 m6 l6 center-align">
- <a class="btn disabled waves-effect waves-light" href="<%= url_for('journey', id => $journey->{id}, format => 'json' ) %>">
- <i class="material-icons left" aria-label="<%= L('journey.map.upload') %>">file_upload</i>
- JSON
- </a>
- </div>
- <div class="col s12 m6 l6 center-align">
- <a class="btn disabled waves-effect waves-light" href="<%= url_for('journey', id => $journey->{id}, format => 'json' ) %>">
- <i class="material-icons left" aria-label="<%= L('journey.map.upload') %>">file_upload</i>
- GPX
- </a>
+ <h2><%= L('journey.map-data') %>
+ % if ($journey->{edited} & 0x0040) {
+ ∗
+ % }
+ </h2>
+ % if ($journey->{polyline}) {
+ <div class="row">
+ <div class="col s12 m6 l6 center-align">
+ <a class="btn waves-effect waves-light" href="<%= url_for('polyline_download', id => $journey->{id}, format => 'json' ) %>">
+ <i class="material-icons left" aria-label="<%= L('journey.map.download') %>">file_download</i>
+ JSON
+ </a>
+ </div>
+ <div class="col s12 m6 l6 center-align">
+ <a class="btn waves-effect waves-light" href="<%= url_for('polyline_download', id => $journey->{id}, format => 'gpx' ) %>">
+ <i class="material-icons left" aria-label="<%= L('journey.map.download') %>">file_download</i>
+ GPX
+ </a>
+ </div>
</div>
- </div>
+ % }
<div class="row">
<div class="col s12">
- %= L('journey.map.info')
+ % if ($journey->{polyline}) {
+ %= L('journey.map.info.download')
+ % }
+ %= L('journey.map.info.upload')
</div>
</div>
+ %= form_for '/polyline/set' => (method => 'post', enctype => 'multipart/form-data') => begin
+ %= csrf_field
+ %= hidden_field id => $journey->{id}
+ <div class="row">
+ <div class="col s12">
+ <div class="file-field input-field">
+ <div class="btn">
+ <span><i class="material-icons left" aria-label="<%= L('journey.map.upload') %>">file_upload</i> GPX</span>
+ <input type="file">
+ </div>
+ <div class="file-path-wrapper">
+ <input class="file-path validate" type="text" name="file">
+ </div>
+ %= file_field 'file'
+ </div>
+ </div>
+ <div class="col s12 m6 center-align">
+ %= submit_button L('journey.map.upload-full'), class => 'waves-effect waves-light btn', name => 'upload-full'
+ </div>
+ <div class="col s12 m6 center-align">
+ %= submit_button L('journey.map.upload-partial'), class => 'waves-effect waves-light btn', name => 'upload-partial'
+ </div>
+ </div>
+ %= end
<h2><%= L('journey.danger') %></h2>
<div class="row">
<div class="col s12 m12 l12 center-align" style="margin-top: 1em;">