diff options
Diffstat (limited to 'lib/Travelynx/Model')
| -rw-r--r-- | lib/Travelynx/Model/InTransit.pm | 337 | ||||
| -rwxr-xr-x | lib/Travelynx/Model/Journeys.pm | 355 | ||||
| -rw-r--r-- | lib/Travelynx/Model/Stations.pm | 37 | ||||
| -rw-r--r-- | lib/Travelynx/Model/Users.pm | 19 |
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, |
