summaryrefslogtreecommitdiff
path: root/lib/Travelynx/Model
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travelynx/Model')
-rw-r--r--lib/Travelynx/Model/InTransit.pm337
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm355
-rw-r--r--lib/Travelynx/Model/Stations.pm37
-rw-r--r--lib/Travelynx/Model/Users.pm19
4 files changed, 635 insertions, 113 deletions
diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm
index b67b716..8324027 100644
--- a/lib/Travelynx/Model/InTransit.pm
+++ b/lib/Travelynx/Model/InTransit.pm
@@ -10,6 +10,7 @@ use warnings;
use 5.020;
use DateTime;
+use GIS::Distance;
use JSON;
my %visibility_itoa = (
@@ -101,6 +102,7 @@ sub add {
my $journey = $opt{journey};
my $stop = $opt{stop};
my $stopover = $opt{stopover};
+ my $manual = $opt{manual};
my $checkin_station_id = $opt{departure_eva};
my $route = $opt{route};
my $data = $opt{data};
@@ -129,7 +131,7 @@ sub add {
messages => $json->encode(
[ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ]
),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $train->departure_has_realtime ? 1
: 0,
@@ -152,19 +154,22 @@ sub add {
$j_stop->full_name,
$j_stop->id_num,
{
- sched_arr => _epoch( $j_stop->sched_arr ),
- sched_dep => _epoch( $j_stop->sched_dep ),
- rt_arr => _epoch( $j_stop->rt_arr ),
- rt_dep => _epoch( $j_stop->rt_dep ),
- isCancelled => $j_stop->is_cancelled,
- arr_delay => $j_stop->arr_delay,
- dep_delay => $j_stop->dep_delay,
- efa_load => $j_stop->occupancy,
- lat => $j_stop->latlon->[0],
- lon => $j_stop->latlon->[1],
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ platform => $j_stop->platform,
+ efa_load => $j_stop->occupancy,
+ lat => $j_stop->latlon->[0],
+ lon => $j_stop->latlon->[1],
}
]
);
+ if ( $j_stop->is_cancelled ) {
+ $route[-1][2]{isCancelled} = 1;
+ }
}
$persistent_data->{operator} = $journey->operator;
$db->insert(
@@ -182,13 +187,13 @@ sub add {
sched_departure => $stop->sched_dep,
real_departure => $stop->rt_dep // $stop->sched_dep,
route => $json->encode( \@route ),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $stop->rt_dep ? 1 : 0,
%{ $data // {} }
}
),
- user_data => JSON->new->encode($persistent_data),
+ user_data => $json->encode($persistent_data),
backend_id => $backend_id,
}
);
@@ -213,6 +218,7 @@ sub add {
rt_dep => _epoch( $j_stop->rt_dep ),
arr_delay => $j_stop->arr_delay,
dep_delay => $j_stop->dep_delay,
+ platform => $j_stop->platform,
load => $j_stop->load,
lat => $j_stop->loc->lat,
lon => $j_stop->loc->lon,
@@ -243,13 +249,13 @@ sub add {
sched_departure => $stop->{sched_dep},
real_departure => $stop->{rt_dep} // $stop->{sched_dep},
route => $json->encode( \@route ),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $stop->{rt_dep} ? 1 : 0,
%{ $data // {} }
}
),
- user_data => JSON->new->encode($persistent_data),
+ user_data => $json->encode($persistent_data),
backend_id => $backend_id,
}
);
@@ -258,7 +264,11 @@ sub add {
and $stop
and ref($journey) eq 'Travel::Status::DE::DBRIS::Journey' )
{
- my $number = $journey->train_no // $journey->number // $train_suffix;
+ my $trip_no
+ = $journey->trip_no_at( $stop->eva,
+ $stop->sched_dep ? $stop->sched_dep->epoch : undef )
+ // $journey->train_no;
+ my $number = $trip_no // $journey->number // $train_suffix;
my $line;
if ( defined $journey->line_no and $journey->line_no ne $number ) {
@@ -276,14 +286,14 @@ sub add {
$j_stop->name,
$j_stop->eva,
{
- sched_arr => _epoch( $j_stop->sched_arr ),
- sched_dep => _epoch( $j_stop->sched_dep ),
- rt_arr => _epoch( $j_stop->rt_arr ),
- rt_dep => _epoch( $j_stop->rt_dep ),
- isCancelled => $j_stop->is_cancelled,
- arr_delay => $j_stop->arr_delay,
- dep_delay => $j_stop->dep_delay,
- load => {
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ platform => $j_stop->platform,
+ load => {
FIRST => $j_stop->occupancy_first,
SECOND => $j_stop->occupancy_second
},
@@ -292,6 +302,12 @@ sub add {
}
]
);
+ if ( $j_stop->is_additional ) {
+ $route[-1][2]{isAdditional} = 1;
+ }
+ if ( $j_stop->is_cancelled ) {
+ $route[-1][2]{isCancelled} = 1;
+ }
}
my @messages;
for my $msg ( $journey->messages ) {
@@ -313,6 +329,12 @@ sub add {
);
}
}
+ if ( scalar $journey->admin_ids ) {
+ $persistent_data->{admin_ids} = [ $journey->admin_ids ];
+ }
+ if ( scalar $journey->operators ) {
+ $persistent_data->{operators} = [ $journey->operators ];
+ }
$db->insert(
'in_transit',
{
@@ -330,13 +352,13 @@ sub add {
sched_departure => $stop->sched_dep,
real_departure => $stop->rt_dep // $stop->sched_dep,
route => $json->encode( \@route ),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $stop->{rt_dep} ? 1 : 0,
%{ $data // {} }
}
),
- user_data => JSON->new->encode($persistent_data),
+ user_data => $json->encode($persistent_data),
backend_id => $backend_id,
}
);
@@ -363,6 +385,7 @@ sub add {
_epoch( $journey_stopover->realtime_departure ),
arr_delay => $journey_stopover->arrival_delay,
dep_delay => $journey_stopover->departure_delay,
+ platform => $journey_stopover->track,
lat => $journey_stopover->stop->lat,
lon => $journey_stopover->stop->lon,
}
@@ -389,17 +412,50 @@ sub add {
sched_departure => $stopover->scheduled_departure,
real_departure => $stopover->departure,
route => $json->encode( \@route ),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $stopover->{is_realtime} ? 1 : 0,
%{ $data // {} }
}
),
- user_data => JSON->new->encode($persistent_data),
+ user_data => $json->encode($persistent_data),
backend_id => $backend_id,
}
);
}
+ elsif ($manual) {
+ if ( $manual->{comment} ) {
+ $persistent_data->{comment} = $manual->{comment};
+ }
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => 0,
+ checkin_station_id => $manual->{dep_id},
+ checkout_station_id => $manual->{arr_id},
+ checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ),
+ train_type => $manual->{train_type},
+ train_no => $manual->{train_no} || q{},
+ train_id => 'manual',
+ train_line => $manual->{train_line} || undef,
+ sched_departure => $manual->{sched_departure},
+ real_departure => $manual->{sched_departure},
+ sched_arrival => $manual->{sched_arrival},
+ real_arrival => $manual->{sched_arrival},
+ route => $json->encode( $manual->{route} // [] ),
+ data => $json->encode(
+ {
+ manual => \1,
+ %{ $data // {} }
+ }
+ ),
+ user_data => $json->encode($persistent_data),
+ backend_id => $backend_id,
+ }
+ );
+ return;
+ }
else {
die('invalid arguments / argument types passed to InTransit->add');
}
@@ -478,6 +534,14 @@ sub postprocess {
$ret->{comment} = $ret->{user_data}{comment};
$ret->{wagongroups} = $ret->{user_data}{wagongroups};
+ if ( $ret->{sched_dep_ts} and $ret->{real_dep_ts} ) {
+ $ret->{dep_delay} = $ret->{real_dep_ts} - $ret->{sched_dep_ts};
+ }
+
+ if ( $ret->{sched_arr_ts} and $ret->{real_arr_ts} ) {
+ $ret->{arr_delay} = $ret->{real_arr_ts} - $ret->{sched_arr_ts};
+ }
+
$ret->{platform_type} = 'Gleis';
if ( $ret->{train_type} and $ret->{train_type} =~ m{ ast | bus | ruf }ix ) {
$ret->{platform_type} = 'Steig';
@@ -595,6 +659,7 @@ sub get {
if ( $opt{with_polyline} and $ret ) {
$ret->{dep_latlon} = [ $ret->{dep_lat}, $ret->{dep_lon} ];
$ret->{arr_latlon} = [ $ret->{arr_lat}, $ret->{arr_lon} ];
+ $ret->{now_latlon} = $self->estimate_trip_position($ret);
}
if ( $opt{with_visibility} and $ret ) {
@@ -736,6 +801,22 @@ sub set_arrival_eva {
);
}
+sub set_arrival_platform {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $platform = $opt{arrival_platform};
+
+ $db->update(
+ 'in_transit',
+ {
+ arr_platform => $platform,
+ },
+ { user_id => $uid }
+ );
+}
+
sub set_arrival_times {
my ( $self, %opt ) = @_;
@@ -1193,15 +1274,15 @@ sub update_arrival_dbris {
$j_stop->name,
$j_stop->eva,
{
- sched_arr => _epoch( $j_stop->sched_arr ),
- sched_dep => _epoch( $j_stop->sched_dep ),
- rt_arr => _epoch( $j_stop->rt_arr ),
- rt_dep => _epoch( $j_stop->rt_dep ),
- platform => $j_stop->platform,
- isCancelled => $j_stop->is_cancelled,
- arr_delay => $j_stop->arr_delay,
- dep_delay => $j_stop->dep_delay,
- load => {
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ platform => $j_stop->platform,
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ platform => $j_stop->platform,
+ load => {
FIRST => $j_stop->occupancy_first,
SECOND => $j_stop->occupancy_second
},
@@ -1210,6 +1291,12 @@ sub update_arrival_dbris {
}
]
);
+ if ( $j_stop->is_additional ) {
+ $route[-1][2]{isAdditional} = 1;
+ }
+ if ( $j_stop->is_cancelled ) {
+ $route[-1][2]{isCancelled} = 1;
+ }
}
# selecting on user_id and train_no avoids a race condition if a user checks
@@ -1218,8 +1305,8 @@ sub update_arrival_dbris {
$db->update(
'in_transit',
{
- real_arrival => $stop->{rt_arr},
- arr_platform => $stop->{platform},
+ real_arrival => $stop->rt_arr,
+ arr_platform => $stop->platform,
route => $json->encode( [@route] ),
data => $json->encode($ephemeral_data),
user_data => $json->encode($persistent_data),
@@ -1261,21 +1348,27 @@ sub update_arrival_efa {
$j_stop->full_name,
$j_stop->id_num,
{
- sched_arr => _epoch( $j_stop->sched_arr ),
- sched_dep => _epoch( $j_stop->sched_dep ),
- rt_arr => _epoch( $j_stop->rt_arr ),
- rt_dep => _epoch( $j_stop->rt_dep ),
- isCancelled => $j_stop->is_cancelled,
- arr_delay => $j_stop->arr_delay,
- dep_delay => $j_stop->dep_delay,
- efa_load => $j_stop->occupancy,
- lat => $j_stop->latlon->[0],
- lon => $j_stop->latlon->[1],
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ platform => $j_stop->platform,
+ efa_load => $j_stop->occupancy,
+ lat => $j_stop->latlon->[0],
+ lon => $j_stop->latlon->[1],
}
]
);
+ if ( $j_stop->is_cancelled ) {
+ $route[-1][2]{isCancelled} = 1;
+ }
}
+ # TODO set efa_load from old route entry if missing in current route entry
+ # (at least in VVO, occupancy data is only provided for future stops)
+
# selecting on user_id and train_no avoids a race condition if a user checks
# into a new train while we are fetching data for their previous journey. In
# this case, the new train would receive data from the previous journey.
@@ -1284,6 +1377,7 @@ sub update_arrival_efa {
{
data => $json->encode($ephemeral_data),
real_arrival => $stop->rt_arr,
+ arr_platform => $stop->platform,
route => $json->encode( [@route] ),
},
{
@@ -1321,6 +1415,7 @@ sub update_arrival_motis {
rt_dep => _epoch( $journey_stopover->realtime_departure ),
arr_delay => $journey_stopover->arrival_delay,
dep_delay => $journey_stopover->departure_delay,
+ platform => $journey_stopover->track,
lat => $journey_stopover->stop->lat,
lon => $journey_stopover->stop->lon,
}
@@ -1334,7 +1429,8 @@ sub update_arrival_motis {
$db->update(
'in_transit',
{
- real_arrival => $stopover->{realtime_arrival},
+ real_arrival => $stopover->realtime_arrival,
+ arr_platform => $stopover->track,
route => $json->encode( [@route] ),
},
{
@@ -1380,6 +1476,7 @@ sub update_arrival_hafas {
rt_dep => _epoch( $j_stop->rt_dep ),
arr_delay => $j_stop->arr_delay,
dep_delay => $j_stop->dep_delay,
+ platform => $j_stop->platform,
load => $j_stop->load,
lat => $j_stop->loc->lat,
lon => $j_stop->loc->lon,
@@ -1406,7 +1503,8 @@ sub update_arrival_hafas {
'in_transit',
{
data => $json->encode($ephemeral_data),
- real_arrival => $stop->{rt_arr},
+ real_arrival => $stop->rt_arr,
+ arr_platform => $stop->platform,
route => $json->encode( [@route] ),
},
{
@@ -1488,4 +1586,141 @@ sub update_visibility {
);
}
+sub estimate_trip_position_between_stops {
+ my ( $self, %opt ) = @_;
+
+ my $time_complete = $opt{now} - $opt{from_ts};
+ my $time_total = $opt{to_ts} - $opt{from_ts};
+ my $ratio = $time_complete / $time_total;
+
+ my $distance = GIS::Distance->new;
+ my $polyline = $opt{polyline};
+ my ( $i_from, $i_to );
+
+ for my $i ( 0 .. $#{$polyline} ) {
+ if ( not defined $i_from
+ and $polyline->[$i][2]
+ and $polyline->[$i][2] == $opt{from}[1] )
+ {
+ $i_from = $i;
+ }
+ elsif ( not defined $i_to
+ and $polyline->[$i][2]
+ and $polyline->[$i][2] == $opt{to}[1] )
+ {
+ $i_to = $i;
+ last;
+ }
+ }
+ if ( defined $i_from and defined $i_to ) {
+ my $total_distance = 0;
+ for my $i ( $i_from + 1 .. $i_to ) {
+ my $prev = $polyline->[ $i - 1 ];
+ my $this = $polyline->[$i];
+ if ( $prev and $this ) {
+ $total_distance
+ += $distance->distance_metal( $prev->[1], $prev->[0],
+ $this->[1], $this->[0] );
+ }
+ }
+
+ my $marker_distance = $total_distance * $ratio;
+ $total_distance = 0;
+ for my $i ( $i_from + 1 .. $i_to ) {
+ my $prev = $polyline->[ $i - 1 ];
+ my $this = $polyline->[$i];
+ if ( $prev and $this ) {
+ my $prev_distance = $total_distance;
+ $total_distance
+ += $distance->distance_metal( $prev->[1], $prev->[0],
+ $this->[1], $this->[0] );
+ if ( $total_distance > $marker_distance ) {
+ my $sub_ratio = 1;
+ if ( $total_distance != $prev_distance ) {
+ $sub_ratio = ( $marker_distance - $prev_distance )
+ / ( $total_distance - $prev_distance );
+ }
+ return (
+ $prev->[1] + ( $this->[1] - $prev->[1] ) * $sub_ratio,
+ $prev->[0] + ( $this->[0] - $prev->[0] ) * $sub_ratio,
+ );
+ }
+ }
+ }
+ }
+ return (
+ $opt{from}[2]{lat} + ( $opt{to}[2]{lat} - $opt{from}[2]{lat} ) * $ratio,
+ $opt{from}[2]{lon} + ( $opt{to}[2]{lon} - $opt{from}[2]{lon} ) * $ratio
+ );
+}
+
+sub estimate_trip_position {
+ my ( $self, $in_transit ) = @_;
+
+ my @now_latlon;
+ my @route = @{ $in_transit->{route} };
+
+ # estimate_train_position runs before postprocess, so all route
+ # timestamps are provided in UNIX seconds and not as DateTime objects.
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' )->epoch;
+
+ my $prev_ts;
+ for my $i ( 0 .. $#route ) {
+ my $ts = $route[$i][2]{rt_arr} // $route[$i][2]{sched_arr}
+ // $route[$i][2]{rt_dep} // $route[$i][2]{sched_dep} // 0;
+ my $ts_dep = $route[$i][2]{rt_dep} // $route[$i][2]{sched_dep}
+ // $route[$i][2]{rt_arr} // $route[$i][2]{sched_arr} // 0;
+ if ( $ts and $ts_dep and $now >= $ts and $now <= $ts_dep ) {
+
+ # Currently at a stop
+ @now_latlon = ( $route[$i][2]{lat}, $route[$i][2]{lon} );
+ last;
+ }
+ if ( $ts
+ and $prev_ts
+ and $now > $prev_ts
+ and $now < $ts )
+ {
+ @now_latlon = $self->estimate_trip_position_between_stops(
+ now => $now,
+ from => $route[ $i - 1 ],
+ from_ts => $prev_ts,
+ to => $route[$i],
+ to_ts => $ts,
+ polyline => $in_transit->{polyline},
+ );
+ last;
+ }
+ $prev_ts = $ts_dep;
+ }
+
+ if ( not @now_latlon
+ and $in_transit->{sched_dep_ts}
+ and $in_transit->{sched_arr_ts} )
+ {
+ my $time_complete = $now
+ - ( $in_transit->{real_dep_ts} // $in_transit->{sched_dep_ts} );
+ my $time_total
+ = ( $in_transit->{real_arr_ts} // $in_transit->{sched_arr_ts} )
+ - ( $in_transit->{real_dep_ts} // $in_transit->{sched_dep_ts} );
+
+ if ( $time_total == 0 ) {
+ return [ $in_transit->{dep_lat}, $in_transit->{dep_lon} ];
+ }
+
+ my $completion = $time_complete / $time_total;
+ $completion = $completion < 0 ? 0 : $completion > 1 ? 1 : $completion;
+ @now_latlon = (
+ $in_transit->{dep_lat}
+ + ( $in_transit->{arr_lat} - $in_transit->{dep_lat} )
+ * $completion,
+ $in_transit->{dep_lon}
+ + ( $in_transit->{arr_lon} - $in_transit->{dep_lon} )
+ * $completion,
+ );
+ }
+
+ return \@now_latlon;
+}
+
1;
diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm
index 0fb663e..bce475f 100755
--- a/lib/Travelynx/Model/Journeys.pm
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -4,16 +4,16 @@ package Travelynx::Model::Journeys;
#
# SPDX-License-Identifier: AGPL-3.0-or-later
-use GIS::Distance;
-use List::MoreUtils qw(after_incl before_incl);
-
use strict;
use warnings;
use 5.020;
use utf8;
use DateTime;
+use DateTime::Format::Strptime;
+use GIS::Distance;
use JSON;
+use List::MoreUtils qw(after_incl before_incl first_index last_index);
my %visibility_itoa = (
100 => 'public',
@@ -50,6 +50,8 @@ sub epoch_to_dt {
);
}
+# TODO turn into a travelynx helper called from templates so that
+# loc_handle is available for localization
sub min_to_human {
my ( $self, $minutes ) = @_;
@@ -183,20 +185,44 @@ sub add {
}
if ( $opt{route} ) {
+ my $parser = DateTime::Format::Strptime->new(
+ pattern => '%d.%m.%Y %H:%M',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
my @unknown_stations;
+ my $prev_epoch = 0;
+
for my $station ( @{ $opt{route} } ) {
+ my $ts;
+ my %station_data;
+ if ( $station
+ =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x )
+ {
+ $station = $+{stop};
+ $ts = $parser->parse_datetime( $+{timestamp} );
+ if ($ts) {
+ my $epoch = $ts->epoch;
+ if ( $epoch < $prev_epoch ) {
+ return ( undef,
+'Zeitstempel der Unterwegshalte müssen monoton steigend sein (keine Zeitreisen und keine Portale)'
+ );
+ }
+ $station_data{sched_arr} = $epoch;
+ $station_data{sched_dep} = $epoch;
+ $prev_epoch = $epoch;
+ }
+ }
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},
- {
- lat => $station_info->{lat},
- lon => $station_info->{lon},
- }
+ $station_info->{name}, $station_info->{eva},
+ \%station_data,
]
);
}
@@ -283,8 +309,14 @@ sub add_from_in_transit {
my $db = $opt{db};
my $journey = $opt{journey};
+ if ( $journey->{train_id} eq 'manual' ) {
+ $journey->{edited} = 0x3fff;
+ }
+ else {
+ $journey->{edited} = 0;
+ }
+
delete $journey->{data};
- $journey->{edited} = 0;
$journey->{checkout_time} = DateTime->now( time_zone => 'Europe/Berlin' );
return $db->insert( 'journeys', $journey, { returning => 'id' } )
@@ -301,16 +333,16 @@ sub update {
my $rows;
my $journey = $self->get_single(
- uid => $uid,
- db => $db,
- journey_id => $journey_id,
- with_datetime => 1,
- with_route_datetime => 1,
+ uid => $uid,
+ db => $db,
+ journey_id => $journey_id,
+ with_datetime => 1,
);
eval {
if ( exists $opt{from_name} ) {
- my $from_station = $self->{stations}->search( $opt{from_name} );
+ my $from_station = $self->{stations}
+ ->search( $opt{from_name}, backend_id => $journey->{backend_id} );
if ( not $from_station ) {
die("Unbekannter Startbahnhof\n");
}
@@ -326,7 +358,8 @@ sub update {
)->rows;
}
if ( exists $opt{to_name} ) {
- my $to_station = $self->{stations}->search( $opt{to_name} );
+ my $to_station = $self->{stations}
+ ->search( $opt{to_name}, backend_id => $journey->{backend_id} );
if ( not $to_station ) {
die("Unbekannter Zielbahnhof\n");
}
@@ -399,7 +432,40 @@ sub update {
)->rows;
}
if ( exists $opt{route} ) {
- my @new_route = map { [ $_, undef, {} ] } @{ $opt{route} };
+
+ # If $opt{route} is a subset of $journey->{route}, we can recycle all data
+ my @new_route;
+ my $new_route_i = 0;
+ for my $old_route_i ( 0 .. $#{ $journey->{route} } ) {
+ if ( $journey->{route}[$old_route_i][0] eq
+ $opt{route}[$new_route_i] )
+ {
+ $new_route_i += 1;
+ push( @new_route, $journey->{route}[$old_route_i] );
+ }
+ }
+
+ # Otherwise, fetch stop IDs so that polylines remain usable
+ if ( @new_route != @{ $opt{route} } ) {
+ my %stop
+ = map { $_->{name} => $_ } $self->{stations}->get_by_names(
+ backend_id => $journey->{backend_id},
+ names => [ $opt{route} ]
+ );
+ @new_route = map {
+ [
+ $_,
+ $stop{$_}{eva},
+ defined $stop{$_}{eva}
+ ? {
+ lat => $stop{$_}{lat},
+ lon => $stop{$_}{lon}
+ }
+ : {}
+ ]
+ } @{ $opt{route} };
+ }
+
$rows = $db->update(
'journeys',
{
@@ -537,6 +603,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 ) = @_;
@@ -549,7 +692,7 @@ sub get {
my @select
= (
- qw(journey_id is_dbris is_iris is_hafas is_motis backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility)
+ qw(journey_id is_dbris is_iris is_hafas is_motis backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_platform dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_platform arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility)
);
my %where = (
user_id => $uid,
@@ -613,12 +756,13 @@ sub get {
is_motis => $entry->{is_motis},
backend_name => $entry->{backend_name},
backend_id => $entry->{backend_id},
- type => $entry->{train_type},
+ type => $entry->{train_type} =~ s{ \s+ $ }{}rx,
line => $entry->{train_line},
no => $entry->{train_no},
from_eva => $entry->{dep_eva},
from_ds100 => $entry->{dep_ds100},
from_name => $entry->{dep_name},
+ from_platform => $entry->{dep_platform},
from_latlon => [ $entry->{dep_lat}, $entry->{dep_lon} ],
checkin_ts => $entry->{checkin_ts},
sched_dep_ts => $entry->{sched_dep_ts},
@@ -626,6 +770,7 @@ sub get {
to_eva => $entry->{arr_eva},
to_ds100 => $entry->{arr_ds100},
to_name => $entry->{arr_name},
+ to_platform => $entry->{arr_platform},
to_latlon => [ $entry->{arr_lat}, $entry->{arr_lon} ],
checkout_ts => $entry->{checkout_ts},
sched_arr_ts => $entry->{sched_arr_ts},
@@ -659,12 +804,18 @@ sub get {
$ref->{checkout} = epoch_to_dt( $ref->{checkout_ts} );
$ref->{sched_arrival} = epoch_to_dt( $ref->{sched_arr_ts} );
$ref->{rt_arrival} = epoch_to_dt( $ref->{rt_arr_ts} );
+ if ( $ref->{rt_dep_ts} and $ref->{sched_dep_ts} ) {
+ $ref->{delay_dep} = $ref->{rt_dep_ts} - $ref->{sched_dep_ts};
+ }
+ if ( $ref->{rt_arr_ts} and $ref->{sched_arr_ts} ) {
+ $ref->{delay_arr} = $ref->{rt_arr_ts} - $ref->{sched_arr_ts};
+ }
}
if ( $opt{with_route_datetime} ) {
for my $stop ( @{ $ref->{route} } ) {
for my $k (qw(rt_arr rt_dep sched_arr sched_dep)) {
if ( $stop->[2]{$k} ) {
- $stop->[2]{$k} = epoch_to_dt( $stop->[2]{$k} );
+ $stop->[2]{"${k}_dt"} = epoch_to_dt( $stop->[2]{$k} );
}
}
}
@@ -1023,6 +1174,8 @@ sub generate_missing_stats {
my $stats_index = 0;
+ my %need_year;
+
for my $journey_index ( 0 .. $#journey_months ) {
if ( $stats_index < @stats_months
and $journey_months[$journey_index][0]
@@ -1034,6 +1187,7 @@ sub generate_missing_stats {
}
else {
my ( $year, $month ) = @{ $journey_months[$journey_index] };
+ $need_year{$year} = 1;
$self->get_stats(
uid => $uid,
db => $db,
@@ -1043,6 +1197,14 @@ sub generate_missing_stats {
);
}
}
+ for my $year ( keys %need_year ) {
+ $self->get_stats(
+ uid => $uid,
+ db => $db,
+ year => $year,
+ write_only => 1
+ );
+ }
}
sub get_nav_months {
@@ -1133,9 +1295,10 @@ sub sanity_check {
. ' Stimmt das wirklich?';
}
if ( $journey->{edited} & 0x0010 and not $lax ) {
- my @unknown_stations
- = $self->{stations}
- ->grep_unknown( map { $_->[0] } @{ $journey->{route} } );
+ my @unknown_stations = $self->{stations}->grep_unknown(
+ backend_id => $journey->{backend_id},
+ names => [ map { $_->[0] } @{ $journey->{route} } ]
+ );
if (@unknown_stations) {
return 'Unbekannte Station(en): ' . join( ', ', @unknown_stations );
}
@@ -1150,9 +1313,11 @@ sub get_travel_distance {
my $from = $journey->{from_name};
my $from_eva = $journey->{from_eva};
my $from_latlon = $journey->{from_latlon};
+ my $from_ts = $journey->{sched_dep_ts} // $journey->{rt_dep_ts};
my $to = $journey->{to_name};
my $to_eva = $journey->{to_eva};
my $to_latlon = $journey->{to_latlon};
+ my $to_ts = $journey->{sched_arr_ts} // $journey->{rt_arr_ts};
my $route_ref = $journey->{route};
my $polyline_ref = $journey->{polyline};
@@ -1206,31 +1371,85 @@ sub get_travel_distance {
my $geo = GIS::Distance->new();
my $distance_beeline
= $geo->distance_metal( @{$from_latlon}, @{$to_latlon} );
- my @route
- = after_incl { ( $_->[1] and $_->[1] == $from_eva ) or $_->[0] eq $from }
+
+ # A trip may pass the same stop multiple times.
+ # Thus, two criteria must be met to select the start/end of the actual route:
+ # * stop name or ID matches, and
+ # * one of:
+ # - arrival/departure time at the stop matches, or
+ # - the stop does not have arrival/departure time
+ # In the latter case, we still face the risk of selecting the wrong
+ # start/end stop. However, we have no way of finding the right one. As the
+ # majority of trips do not pass the same stop multiple times, it's better
+ # to risk having a few inaccurate distances than not calculating the
+ # distance for any journey that lacks sched_dep/rt_dep or
+ # sched_from/rt_from.
+
+ my $route_start = first_index {
+ (
+ ( $_->[1] and $_->[1] == $from_eva or $_->[0] eq $from )
+ and ( not( defined $_->[2]{sched_dep} or defined $_->[2]{rt_dep} )
+ or ( $_->[2]{sched_dep} // $_->[2]{rt_dep} ) == $from_ts )
+ )
+ }
@{$route_ref};
- @route
- = before_incl { ( $_->[1] and $_->[1] == $to_eva ) or $_->[0] eq $to }
- @route;
- if (
- @route < 2
- or ( $route[-1][0] ne $to
- and ( not $route[-1][1] or $route[-1][1] != $to_eva ) )
- )
- {
+ # Here, we need to use last_index. In case of ring lines, the first index
+ # will not have sched_arr/rt_arr set, but we should not select it as route
+ # end...
+ my $route_end = last_index {
+ (
+ ( $_->[1] and $_->[1] == $to_eva or $_->[0] eq $to )
+ and ( not( defined $_->[2]{sched_arr} or defined $_->[2]{rt_arr} )
+ or ( $_->[2]{sched_arr} // $_->[2]{rt_arr} ) == $to_ts )
+ )
+ }
+ @{$route_ref};
- # I AM ERROR
+ if ( not defined $route_start and defined $route_end ) {
return ( 0, 0, $distance_beeline );
}
- my @polyline = after_incl { $_->[2] and $_->[2] == $from_eva }
+ my %seen;
+ for my $stop ( @{$route_ref} ) {
+ if ( not defined $stop->[1] ) {
+ return ( 0, 0, $distance_beeline );
+ }
+ $seen{ $stop->[1] } //= 1;
+ $stop->[2]{n} = $seen{ $stop->[1] };
+ $seen{ $stop->[1] } += 1;
+ }
+
+ # Assumption: polyline entries are always [lat, lon] or [lat, lon, stop ID]
+ %seen = ();
+ for my $entry ( @{ $polyline_ref // [] } ) {
+ if ( $entry->[2] ) {
+ $seen{ $entry->[2] } //= 1;
+ $entry->[3] = $seen{ $entry->[2] };
+ $seen{ $entry->[2] } += 1;
+ }
+ }
+
+ $journey->{route_dep_index} = $route_start;
+ $journey->{route_arr_index} = $route_end;
+
+ my @route = @{$route_ref}[ $route_start .. $route_end ];
+
+ # Just like the route, the polyline may contain the same stop more than
+ # once. So we need to select based on the seen counter.
+ my $poly_start = first_index {
+ $_->[2] and $_->[2] == $from_eva and $_->[3] == $route[0][2]{n}
+ }
+ @{ $polyline_ref // [] };
+ my $poly_end = first_index {
+ $_->[2] and $_->[2] == $to_eva and $_->[3] == $route[-1][2]{n}
+ }
@{ $polyline_ref // [] };
- @polyline
- = before_incl { $_->[2] and $_->[2] == $to_eva } @polyline;
- # ensure that before_incl matched -- otherwise, @polyline is too long
- if ( @polyline and $polyline[-1][2] == $to_eva ) {
+ if ( defined $poly_start and defined $poly_end ) {
+ $journey->{poly_dep_index} = $poly_start;
+ $journey->{poly_arr_index} = $poly_end;
+ my @polyline = @{$polyline_ref}[ $poly_start .. $poly_end ];
my $prev_station = shift @polyline;
for my $station (@polyline) {
$distance_polyline += $geo->distance_metal(
@@ -1850,6 +2069,34 @@ 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;
+
+ return @backend_ids;
+}
+
# Returns a listref of {eva, name} hashrefs for the specified backend.
sub get_connection_targets {
my ( $self, %opt ) = @_;
@@ -1857,9 +2104,14 @@ sub get_connection_targets {
my $uid = $opt{uid};
my $threshold = $opt{threshold}
// DateTime->now( time_zone => 'Europe/Berlin' )->subtract( months => 4 );
- my $db = $opt{db} //= $self->{pg}->db;
- my $min_count = $opt{min_count} // 3;
- my $dest_id = $opt{eva};
+ my $db = $opt{db} //= $self->{pg}->db;
+ my $min_count = $opt{min_count} // 3;
+ my $backend_id = $opt{backend_id};
+ my $dest_id = $opt{eva};
+
+ $self->{log}->debug(
+"get_connection_targets(uid => $uid, backend_id => $backend_id, dest_id => $dest_id)"
+ );
if ( $opt{destination_name} ) {
return {
@@ -1868,8 +2120,6 @@ sub get_connection_targets {
};
}
- my $backend_id = $opt{backend_id};
-
if ( not $dest_id ) {
( $dest_id, $backend_id ) = $self->get_latest_dest_ids(%opt);
}
@@ -1900,10 +2150,15 @@ sub get_connection_targets {
order_by => { -desc => 'count' }
}
);
- my @destinations
- = $res->hashes->grep( sub { shift->{count} >= $min_count } )
- ->map( sub { shift->{dest} } )
- ->each;
+ my @all_destinations = $res->hashes->each;
+ my @destinations;
+
+ while ( not @destinations and $min_count > 0 ) {
+ @destinations = map { $_->{dest} }
+ grep { $_->{count} >= $min_count } @all_destinations;
+ $min_count--;
+ }
+
@destinations = $self->{stations}->get_by_evas(
backend_id => $opt{backend_id},
evas => [@destinations]
diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm
index bf35d1a..6c647ec 100644
--- a/lib/Travelynx/Model/Stations.pm
+++ b/lib/Travelynx/Model/Stations.pm
@@ -205,6 +205,9 @@ sub add_or_update {
);
return;
}
+ if (not $stop->latlon) {
+ die('Backend Error: Stop "' . $stop->full_name . '" has no geo coordinates');
+ }
$opt{db}->insert(
'stations',
{
@@ -458,11 +461,16 @@ sub get_by_name {
# Slow
sub get_by_names {
- my ( $self, @names ) = @_;
+ my ( $self, %opt ) = @_;
- my @ret
- = $self->{pg}->db->select( 'stations', '*', { name => { '=', \@names } } )
- ->hashes->each;
+ my @ret = $self->{pg}->db->select(
+ 'stations',
+ '*',
+ {
+ name => { '=', $opt{names} },
+ source => $opt{backend_id}
+ }
+ )->hashes->each;
return @ret;
}
@@ -503,12 +511,27 @@ sub search {
# Slow
sub grep_unknown {
- my ( $self, @stations ) = @_;
+ my ( $self, %opt ) = @_;
- my %station = map { $_->{name} => 1 } $self->get_by_names(@stations);
- my @unknown_stations = grep { not $station{$_} } @stations;
+ my %station = map { $_->{name} => 1 } $self->get_by_names(%opt);
+ my @unknown_stations = grep { not $station{$_} } @{ $opt{names} };
return @unknown_stations;
}
+sub get_bahn_stationinfo {
+ my ( $self, %opt ) = @_;
+ $opt{db} //= $self->{pg}->db;
+
+ my $res
+ = $opt{db}
+ ->select( 'bahn_platform_directions', ['data'], { eva => $opt{eva} } )
+ ->expand->hash;
+
+ if ($res) {
+ return $res->{data};
+ }
+ return;
+}
+
1;
diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm
index be9e80b..3ef7f33 100644
--- a/lib/Travelynx/Model/Users.pm
+++ b/lib/Travelynx/Model/Users.pm
@@ -216,6 +216,14 @@ sub set_backend {
);
}
+sub set_language {
+ my ( $self, %opt ) = @_;
+ $opt{db} //= $self->{pg}->db;
+
+ $opt{db}
+ ->update( 'users', { language => $opt{language} }, { id => $opt{uid} } );
+}
+
sub set_privacy {
my ( $self, %opt ) = @_;
my $db = $opt{db} // $self->{pg}->db;
@@ -413,7 +421,7 @@ sub get {
my $user = $db->select(
'users_with_backend',
- 'id, name, status, public_level, email, '
+ 'id, name, status, public_level, email, language, '
. 'accept_follows, notifications, '
. 'extract(epoch from registered_at) as registered_at_ts, '
. 'extract(epoch from last_seen) as last_seen_ts, '
@@ -423,10 +431,11 @@ sub get {
)->hash;
if ($user) {
return {
- id => $user->{id},
- name => $user->{name},
- status => $user->{status},
- notifications => $user->{notifications},
+ id => $user->{id},
+ name => $user->{name},
+ languages => [ split( qr{[|]}, $user->{language} // q{} ) ],
+ status => $user->{status},
+ notifications => $user->{notifications},
accept_follows => $user->{accept_follows} == 2 ? 1 : 0,
accept_follow_requests => $user->{accept_follows} == 1 ? 1 : 0,
default_visibility => $user->{public_level} & 0x7f,