diff options
Diffstat (limited to 'lib/Travelynx/Model')
-rw-r--r-- | lib/Travelynx/Model/InTransit.pm | 824 | ||||
-rwxr-xr-x | lib/Travelynx/Model/Journeys.pm | 353 | ||||
-rw-r--r-- | lib/Travelynx/Model/Stations.pm | 404 | ||||
-rw-r--r-- | lib/Travelynx/Model/Traewelling.pm | 1 | ||||
-rw-r--r-- | lib/Travelynx/Model/Users.pm | 58 |
5 files changed, 1382 insertions, 258 deletions
diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm index 69026ac..11177dd 100644 --- a/lib/Travelynx/Model/InTransit.pm +++ b/lib/Travelynx/Model/InTransit.pm @@ -1,6 +1,7 @@ package Travelynx::Model::InTransit; -# Copyright (C) 2020-2023 Birte Kristina Friesel +# Copyright (C) 2020-2025 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -12,11 +13,12 @@ use DateTime; use JSON; my %visibility_itoa = ( - 100 => 'public', - 80 => 'travelynx', - 60 => 'followers', - 30 => 'unlisted', - 10 => 'private', + 100 => 'public', + 80 => 'travelynx', + 60 => 'followers', + 30 => 'unlisted', + 10 => 'private', + default => 'default', ); my %visibility_atoi = ( @@ -30,7 +32,7 @@ my %visibility_atoi = ( sub _epoch { my ($dt) = @_; - return $dt ? $dt->epoch : 0; + return $dt ? $dt->epoch : undef; } sub epoch_to_dt { @@ -47,6 +49,16 @@ sub epoch_to_dt { ); } +sub epoch_or_dt_to_dt { + my ($input) = @_; + + if ( ref($input) eq 'DateTime' ) { + return $input; + } + + return epoch_to_dt($input); +} + sub new { my ( $class, %opt ) = @_; @@ -83,13 +95,20 @@ sub add { my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; + my $backend_id = $opt{backend_id}; my $train = $opt{train}; + my $train_suffix = $opt{train_suffix}; 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}; + my $persistent_data; my $json = JSON->new; + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); if ($train) { $db->insert( @@ -99,28 +118,86 @@ sub add { cancelled => $train->departure_is_cancelled ? 1 : 0, checkin_station_id => $checkin_station_id, - checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ), - dep_platform => $train->platform, - train_type => $train->type, - train_line => $train->line_no, - train_no => $train->train_no, - train_id => $train->train_id, - sched_departure => $train->sched_departure, - real_departure => $train->departure, - route => $json->encode($route), - messages => $json->encode( + checkin_time => $now, + dep_platform => $train->platform, + train_type => $train->type, + train_line => $train->line_no, + train_no => $train->train_no, + train_id => $train->train_id, + sched_departure => $train->sched_departure, + real_departure => $train->departure, + route => $json->encode($route), + messages => $json->encode( [ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ] ), - data => JSON->new->encode( + data => $json->encode( { rt => $train->departure_has_realtime ? 1 - : 0 + : 0, + %{ $data // {} } } ), + backend_id => $backend_id, } ); } - elsif ( $journey and $stop ) { + elsif ( $journey + and $stop + and ref($journey) eq 'Travel::Status::DE::EFA::Trip' ) + { + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $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], + } + ] + ); + } + $persistent_data->{operator} = $journey->operator; + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => $stop->is_cancelled ? 1 : 0, + checkin_station_id => $stop->id_num, + checkin_time => $now, + dep_platform => $stop->platform, + train_type => $journey->type // q{}, + train_line => $journey->line, + train_no => $journey->number // q{}, + train_id => $opt{trip_id}, + sched_departure => $stop->sched_dep, + real_departure => $stop->rt_dep // $stop->sched_dep, + route => $json->encode( \@route ), + data => $json->encode( + { + rt => $stop->rt_dep ? 1 : 0, + %{ $data // {} } + } + ), + user_data => $json->encode($persistent_data), + backend_id => $backend_id, + } + ); + } + elsif ( $journey + and $stop + and ref($journey) eq 'Travel::Status::DE::HAFAS::Journey' ) + { my @route; my $product = $journey->product_at( $stop->loc->eva ) // $journey->product; @@ -137,7 +214,9 @@ sub add { rt_dep => _epoch( $j_stop->rt_dep ), arr_delay => $j_stop->arr_delay, dep_delay => $j_stop->dep_delay, - load => $j_stop->load + load => $j_stop->load, + lat => $j_stop->loc->lat, + lon => $j_stop->loc->lon, } ] ); @@ -145,6 +224,9 @@ sub add { $route[-1][2]{tz_offset} = $j_stop->tz_offset; } } + if ( scalar $journey->operators ) { + $persistent_data->{operators} = [ $journey->operators ]; + } $db->insert( 'in_transit', { @@ -153,21 +235,207 @@ sub add { ? 1 : 0, checkin_station_id => $stop->loc->eva, + checkin_time => $now, + dep_platform => $stop->{platform}, + train_type => $product->type // q{}, + train_line => $product->line_no, + train_no => $product->number // q{}, + train_id => $journey->id, + sched_departure => $stop->{sched_dep}, + real_departure => $stop->{rt_dep} // $stop->{sched_dep}, + route => $json->encode( \@route ), + data => $json->encode( + { + rt => $stop->{rt_dep} ? 1 : 0, + %{ $data // {} } + } + ), + user_data => $json->encode($persistent_data), + backend_id => $backend_id, + } + ); + } + elsif ( $journey + and $stop + and ref($journey) eq 'Travel::Status::DE::DBRIS::Journey' ) + { + my $number = $journey->train_no // $journey->number // $train_suffix; + + my $line; + if ( defined $journey->line_no and $journey->line_no ne $number ) { + $line = $journey->line_no; + } + elsif ( defined $train_suffix and $train_suffix ne $number ) { + $line = $train_suffix; + } + + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $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 => { + FIRST => $j_stop->occupancy_first, + SECOND => $j_stop->occupancy_second + }, + lat => $j_stop->lat, + lon => $j_stop->lon, + } + ] + ); + } + my @messages; + for my $msg ( $journey->messages ) { + if ( not $msg->{ueberschrift} ) { + push( + @{ $data->{him_msg} }, + { + header => q{}, + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + push( + @{ $persistent_data->{him_msg} }, + { + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + } + } + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => $stop->is_cancelled + ? 1 + : 0, + checkin_station_id => $stop->eva, + checkin_time => $now, + dep_platform => $stop->platform, + train_type => $journey->type // q{}, + train_line => $line, + train_no => $number, + train_id => $data->{trip_id}, + sched_departure => $stop->sched_dep, + real_departure => $stop->rt_dep // $stop->sched_dep, + route => $json->encode( \@route ), + data => $json->encode( + { + rt => $stop->{rt_dep} ? 1 : 0, + %{ $data // {} } + } + ), + user_data => $json->encode($persistent_data), + backend_id => $backend_id, + } + ); + } + elsif ( $journey + and $stopover + and ref($journey) eq 'Travel::Status::MOTIS::Trip' ) + { + my @route; + for my $journey_stopover ( $journey->stopovers ) { + push( + @route, + [ + $journey_stopover->stop->name, + $journey_stopover->stop->{eva} + // die('eva not set for stopover'), + { + sched_arr => + _epoch( $journey_stopover->scheduled_arrival ), + sched_dep => + _epoch( $journey_stopover->scheduled_departure ), + rt_arr => _epoch( $journey_stopover->realtime_arrival ), + rt_dep => + _epoch( $journey_stopover->realtime_departure ), + arr_delay => $journey_stopover->arrival_delay, + dep_delay => $journey_stopover->departure_delay, + lat => $journey_stopover->stop->lat, + lon => $journey_stopover->stop->lon, + } + ] + ); + } + + $persistent_data->{operator} = $journey->agency; + + $db->insert( + 'in_transit', + { + user_id => $uid, + cancelled => $stopover->{is_cancelled} + ? 1 + : 0, + checkin_station_id => $stopover->stop->{eva}, checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ), - dep_platform => $stop->{platform}, - train_type => $product->type // q{}, - train_line => $product->line_no, - train_no => $product->number // q{}, + dep_platform => $stopover->track, + train_type => $journey->mode, + train_no => q{}, train_id => $journey->id, - sched_departure => $stop->{sched_dep}, - real_departure => $stop->{rt_dep} // $stop->{sched_dep}, + train_line => $journey->route_name, + sched_departure => $stopover->scheduled_departure, + real_departure => $stopover->departure, route => $json->encode( \@route ), - data => JSON->new->encode( { rt => $stop->{rt_dep} ? 1 : 0 } ), + data => $json->encode( + { + rt => $stopover->{is_realtime} ? 1 : 0, + %{ $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('neither train nor journey specified'); + die('invalid arguments / argument types passed to InTransit->add'); } } @@ -211,8 +479,15 @@ sub postprocess { if ($is_after) { push( @route_after, $station ); } - if ( $ret->{dep_name} - and $station->[0] eq $ret->{dep_name} ) + + # Note that the departure stop may be present more than once in @route, + # e.g. when traveling along ring lines such as S41 / S42 in Berlin. + if ( + $ret->{dep_name} + and $station->[0] eq $ret->{dep_name} + and not($station->[2]{sched_dep} + and $station->[2]{sched_dep} < $ret->{sched_dep_ts} ) + ) { $is_after = 1; if ( @{$station} > 1 and not $dep_info ) { @@ -235,13 +510,17 @@ sub postprocess { $ret->{route_after} = \@route_after; $ret->{extra_data} = $ret->{data}; $ret->{comment} = $ret->{user_data}{comment}; + $ret->{wagongroups} = $ret->{user_data}{wagongroups}; + + $ret->{platform_type} = 'Gleis'; + if ( $ret->{train_type} and $ret->{train_type} =~ m{ ast | bus | ruf }ix ) { + $ret->{platform_type} = 'Steig'; + } $ret->{visibility_str} - = $ret->{visibility} - ? $visibility_itoa{ $ret->{visibility} } - : 'default'; + = $visibility_itoa{ $ret->{visibility} // 'default' }; $ret->{effective_visibility_str} - = $visibility_itoa{ $ret->{effective_visibility} }; + = $visibility_itoa{ $ret->{effective_visibility} // 'default' }; my @parsed_messages; for my $message ( @{ $ret->{messages} // [] } ) { @@ -265,7 +544,7 @@ sub postprocess { = $dep_info->{rt_arr}->epoch - $epoch; } - for my $station (@route_after) { + for my $station (@route) { if ( @{$station} > 1 ) { # Note: $station->[2]{sched_arr} may already have been @@ -273,31 +552,25 @@ sub postprocess { # station is present several times in a train's route, e.g. # for Frankfurt Flughafen in some nightly connections. my $times = $station->[2] // {}; - if ( $times->{sched_arr} - and ref( $times->{sched_arr} ) ne 'DateTime' ) - { - $times->{sched_arr} - = epoch_to_dt( $times->{sched_arr} ); - if ( $times->{rt_arr} ) { - $times->{rt_arr} - = epoch_to_dt( $times->{rt_arr} ); - $times->{arr_delay} - = $times->{rt_arr}->epoch - $times->{sched_arr}->epoch; + for my $key (qw(sched_arr rt_arr sched_dep rt_dep)) { + if ( $times->{$key} ) { + $times->{$key} + = epoch_or_dt_to_dt( $times->{$key} ); } + } + if ( $times->{sched_arr} and $times->{rt_arr} ) { + $times->{arr_delay} + = $times->{rt_arr}->epoch - $times->{sched_arr}->epoch; + } + if ( $times->{sched_arr} or $times->{rt_arr} ) { $times->{arr} = $times->{rt_arr} || $times->{sched_arr}; $times->{arr_countdown} = $times->{arr}->epoch - $epoch; } - if ( $times->{sched_dep} - and ref( $times->{sched_dep} ) ne 'DateTime' ) - { - $times->{sched_dep} - = epoch_to_dt( $times->{sched_dep} ); - if ( $times->{rt_dep} ) { - $times->{rt_dep} - = epoch_to_dt( $times->{rt_dep} ); - $times->{dep_delay} - = $times->{rt_dep}->epoch - $times->{sched_dep}->epoch; - } + if ( $times->{sched_dep} and $times->{rt_dep} ) { + $times->{dep_delay} + = $times->{rt_dep}->epoch - $times->{sched_dep}->epoch; + } + if ( $times->{sched_dep} or $times->{rt_dep} ) { $times->{dep} = $times->{rt_dep} || $times->{sched_dep}; $times->{dep_countdown} = $times->{dep}->epoch - $epoch; } @@ -339,7 +612,7 @@ sub get { my $table = 'in_transit'; - if ( $opt{with_timestamps} ) { + if ( $opt{with_timestamps} or $opt{with_polyline} ) { $table = 'in_transit_str'; } @@ -353,13 +626,16 @@ sub get { $ret = $res->hash; } + if ( $opt{with_polyline} and $ret ) { + $ret->{dep_latlon} = [ $ret->{dep_lat}, $ret->{dep_lon} ]; + $ret->{arr_latlon} = [ $ret->{arr_lat}, $ret->{arr_lon} ]; + } + if ( $opt{with_visibility} and $ret ) { $ret->{visibility_str} - = $ret->{visibility} - ? $visibility_itoa{ $ret->{visibility} } - : 'default'; + = $visibility_itoa{ $ret->{visibility} // 'default' }; $ret->{effective_visibility_str} - = $visibility_itoa{ $ret->{effective_visibility} }; + = $visibility_itoa{ $ret->{effective_visibility} // 'default' }; } if ( $opt{postprocess} and $ret ) { @@ -408,17 +684,20 @@ sub get_all_active { ->hashes->each; } -sub get_checkout_station_id { +sub get_checkout_ids { my ( $self, %opt ) = @_; my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; - my $status = $db->select( 'in_transit', ['checkout_station_id'], - { user_id => $uid } )->hash; + my $status = $db->select( + 'in_transit', + [ 'checkout_station_id', 'backend_id' ], + { user_id => $uid } + )->hash; if ($status) { - return $status->{checkout_station_id}; + return $status->{checkout_station_id}, $status->{backend_id}; } return; } @@ -457,13 +736,6 @@ sub set_arrival { my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; my $train = $opt{train}; - my $route = $opt{route}; - - $route = $self->_merge_old_route( - db => $db, - uid => $uid, - route => $route - ); my $json = JSON->new; @@ -474,7 +746,6 @@ sub set_arrival { arr_platform => $train->platform, sched_arrival => $train->sched_arrival, real_arrival => $train->arrival, - route => $json->encode($route), messages => $json->encode( [ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ] ) @@ -566,7 +837,8 @@ sub set_polyline { $self->set_polyline_id( uid => $uid, db => $db, - polyline_id => $polyline_id + polyline_id => $polyline_id, + train_id => $opt{train_id}, ); } @@ -579,11 +851,13 @@ sub set_polyline_id { my $db = $opt{db} // $self->{pg}->db; my $polyline_id = $opt{polyline_id}; - $db->update( - 'in_transit', - { polyline_id => $polyline_id }, - { user_id => $uid } - ); + my %where = ( user_id => $uid ); + + if ( $opt{train_id} ) { + $where{train_id} = $opt{train_id}; + } + + $db->update( 'in_transit', { polyline_id => $polyline_id }, \%where ); } sub set_route_data { @@ -596,6 +870,12 @@ sub set_route_data { my $qos_msg = $opt{qos_messages}; my $him_msg = $opt{him_messages}; + my %where = ( user_id => $uid ); + + if ( $opt{train_id} ) { + $where{train_id} = $opt{train_id}; + } + my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } ) ->expand->hash; @@ -612,7 +892,7 @@ sub set_route_data { route => JSON->new->encode($route), data => JSON->new->encode($data) }, - { user_id => $uid } + \%where ); } @@ -699,6 +979,128 @@ sub update_departure_cancelled { return $rows; } +sub update_departure_dbris { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h = $db->select( 'in_transit', [ 'data', 'user_data' ], + { user_id => $uid } )->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + my $persistent_data = $res_h ? $res_h->{user_data} : {}; + + if ( $stop->{rt_dep} ) { + $ephemeral_data->{rt} = 1; + } + + $ephemeral_data->{him_msg} = []; + $persistent_data->{him_msg} = []; + for my $msg ( $journey->messages ) { + if ( not $msg->{ueberschrift} ) { + push( + @{ $ephemeral_data->{him_msg} }, + { + header => q{}, + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + push( + @{ $persistent_data->{him_msg} }, + { + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + } + } + + # 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. + $db->update( + 'in_transit', + { + real_departure => $stop->{rt_dep}, + data => $json->encode($ephemeral_data), + user_data => $json->encode($persistent_data), + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + +sub update_departure_efa { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + if ( $stop->rt_dep ) { + $ephemeral_data->{rt} = 1; + } + + # 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. + $db->update( + 'in_transit', + { + data => $json->encode($ephemeral_data), + real_departure => $stop->rt_dep, + }, + { + user_id => $uid, + train_id => $opt{trip_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + +sub update_departure_motis { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stopover = $opt{stopover}; + my $json = JSON->new; + + # 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. + $db->update( + 'in_transit', + { + real_departure => $stopover->{realtime_departure}, + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_departure_hafas { my ( $self, %opt ) = @_; my $uid = $opt{uid}; @@ -709,12 +1111,20 @@ sub update_departure_hafas { my $stop = $opt{stop}; my $json = JSON->new; + my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + if ( $stop->{rt_dep} ) { + $ephemeral_data->{rt} = 1; + } + # 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. $db->update( 'in_transit', { + data => $json->encode($ephemeral_data), real_departure => $stop->{rt_dep}, }, { @@ -768,6 +1178,210 @@ sub update_arrival { return $rows; } +sub update_arrival_dbris { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h = $db->select( 'in_transit', [ 'data', 'user_data' ], + { user_id => $uid } )->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + my $persistent_data = $res_h ? $res_h->{user_data} : {}; + + if ( $stop->{rt_arr} ) { + $ephemeral_data->{rt} = 1; + } + + $ephemeral_data->{him_msg} = []; + $persistent_data->{him_msg} = []; + for my $msg ( $journey->messages ) { + if ( not $msg->{ueberschrift} ) { + push( + @{ $ephemeral_data->{him_msg} }, + { + header => q{}, + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + push( + @{ $persistent_data->{him_msg} }, + { + prio => $msg->{prioritaet}, + lead => $msg->{text} + } + ); + } + } + + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $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 => { + FIRST => $j_stop->occupancy_first, + SECOND => $j_stop->occupancy_second + }, + lat => $j_stop->lat, + lon => $j_stop->lon, + } + ] + ); + } + + # 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. + $db->update( + 'in_transit', + { + real_arrival => $stop->rt_arr, + arr_platform => $stop->platform, + route => $json->encode( [@route] ), + data => $json->encode($ephemeral_data), + user_data => $json->encode($persistent_data), + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + +sub update_arrival_efa { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stop = $opt{stop}; + my $json = JSON->new; + + my $res_h + = $db->select( 'in_transit', [ 'data', 'route' ], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + my $old_route = $res_h ? $res_h->{route} : []; + + if ( $stop->rt_arr ) { + $ephemeral_data->{rt} = 1; + } + + my @route; + for my $j_stop ( $journey->route ) { + push( + @route, + [ + $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], + } + ] + ); + } + + # 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. + $db->update( + 'in_transit', + { + data => $json->encode($ephemeral_data), + real_arrival => $stop->rt_arr, + arr_platform => $stop->platform, + route => $json->encode( [@route] ), + }, + { + user_id => $uid, + train_id => $opt{trip_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + +sub update_arrival_motis { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $dep_eva = $opt{dep_eva}; + my $arr_eva = $opt{arr_eva}; + my $journey = $opt{journey}; + my $stopover = $opt{stopover}; + my $json = JSON->new; + + my @route; + for my $journey_stopover ( $journey->stopovers ) { + push( + @route, + [ + $journey_stopover->stop->name, + $journey_stopover->stop->{eva} + // die('eva not set for stopover'), + { + sched_arr => _epoch( $journey_stopover->scheduled_arrival ), + sched_dep => + _epoch( $journey_stopover->scheduled_departure ), + rt_arr => _epoch( $journey_stopover->realtime_arrival ), + rt_dep => _epoch( $journey_stopover->realtime_departure ), + arr_delay => $journey_stopover->arrival_delay, + dep_delay => $journey_stopover->departure_delay, + lat => $journey_stopover->stop->lat, + lon => $journey_stopover->stop->lon, + } + ] + ); + } + + # 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. + $db->update( + 'in_transit', + { + real_arrival => $stopover->realtime_arrival, + arr_platform => $stopover->track, + route => $json->encode( [@route] ), + }, + { + user_id => $uid, + train_id => $opt{train_id}, + checkin_station_id => $dep_eva, + checkout_station_id => $arr_eva, + } + ); +} + sub update_arrival_hafas { my ( $self, %opt ) = @_; my $uid = $opt{uid}; @@ -778,7 +1392,16 @@ sub update_arrival_hafas { my $stop = $opt{stop}; my $json = JSON->new; - # TODO use old rt data if available + my $res_h + = $db->select( 'in_transit', [ 'data', 'route' ], { user_id => $uid } ) + ->expand->hash; + my $ephemeral_data = $res_h ? $res_h->{data} : {}; + my $old_route = $res_h ? $res_h->{route} : []; + + if ( $stop->{rt_arr} ) { + $ephemeral_data->{rt} = 1; + } + my @route; for my $j_stop ( $journey->route ) { push( @@ -793,7 +1416,9 @@ sub update_arrival_hafas { rt_dep => _epoch( $j_stop->rt_dep ), arr_delay => $j_stop->arr_delay, dep_delay => $j_stop->dep_delay, - load => $j_stop->load + load => $j_stop->load, + lat => $j_stop->loc->lat, + lon => $j_stop->loc->lon, } ] ); @@ -802,10 +1427,6 @@ sub update_arrival_hafas { } } - my $res_h = $db->select( 'in_transit', ['route'], { user_id => $uid } ) - ->expand->hash; - my $old_route = $res_h ? $res_h->{route} : []; - for my $i ( 0 .. $#route ) { if ( $old_route->[$i] and $old_route->[$i][1] == $route[$i][1] ) { for my $k (qw(rt_arr rt_dep arr_delay dep_delay)) { @@ -820,7 +1441,9 @@ sub update_arrival_hafas { $db->update( 'in_transit', { - real_arrival => $stop->{rt_arr}, + data => $json->encode($ephemeral_data), + real_arrival => $stop->rt_arr, + arr_platform => $stop->platform, route => $json->encode( [@route] ), }, { @@ -839,6 +1462,12 @@ sub update_data { my $db = $opt{db} // $self->{pg}->db; my $new_data = $opt{data} // {}; + my %where = ( user_id => $uid ); + + if ( $opt{train_id} ) { + $where{train_id} = $opt{train_id}; + } + my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } ) ->expand->hash; @@ -848,11 +1477,7 @@ sub update_data { $data->{$k} = $v; } - $db->update( - 'in_transit', - { data => JSON->new->encode($data) }, - { user_id => $uid } - ); + $db->update( 'in_transit', { data => JSON->new->encode($data) }, \%where ); } sub update_user_data { @@ -862,6 +1487,12 @@ sub update_user_data { my $db = $opt{db} // $self->{pg}->db; my $new_data = $opt{user_data} // {}; + my %where = ( user_id => $uid ); + + if ( $opt{train_id} ) { + $where{train_id} = $opt{train_id}; + } + my $res_h = $db->select( 'in_transit', ['user_data'], { user_id => $uid } ) ->expand->hash; @@ -871,11 +1502,8 @@ sub update_user_data { $data->{$k} = $v; } - $db->update( - 'in_transit', - { user_data => JSON->new->encode($data) }, - { user_id => $uid } - ); + $db->update( 'in_transit', + { user_data => JSON->new->encode($data) }, \%where ); } sub update_visibility { diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index 97c4681..b07511a 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); my %visibility_itoa = ( 100 => 'public', @@ -118,8 +118,10 @@ sub add { my $db = $opt{db}; my $uid = $opt{uid}; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); - my $dep_station = $self->{stations}->search( $opt{dep_station} ); - my $arr_station = $self->{stations}->search( $opt{arr_station} ); + my $dep_station = $self->{stations} + ->search( $opt{dep_station}, backend_id => $opt{backend_id} ); + my $arr_station = $self->{stations} + ->search( $opt{arr_station}, backend_id => $opt{backend_id} ); if ( not $dep_station ) { return ( undef, 'Unbekannter Startbahnhof' ); @@ -167,16 +169,60 @@ sub add { my @route; if ( not $route_has_start ) { - push( @route, [ $dep_station->{name}, $dep_station->{eva}, {} ] ); + push( + @route, + [ + $dep_station->{name}, + $dep_station->{eva}, + { + lat => $dep_station->{lat}, + lon => $dep_station->{lon}, + } + ] + ); } 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 $station_info = $self->{stations}->search($station); + 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) { - push( @route, - [ $station_info->{name}, $station_info->{eva}, {} ] ); + $station_data{lat} = $station_info->{lat}; + $station_data{lon} = $station_info->{lon}; + push( + @route, + [ + $station_info->{name}, $station_info->{eva}, + \%station_data, + ] + ); } else { push( @route, [ $station, undef, {} ] ); @@ -198,7 +244,17 @@ sub add { } if ( not $route_has_stop ) { - push( @route, [ $arr_station->{name}, $arr_station->{eva}, {} ] ); + push( + @route, + [ + $arr_station->{name}, + $arr_station->{eva}, + { + lat => $arr_station->{lat}, + lon => $arr_station->{lon}, + } + ] + ); } my $entry = { @@ -218,6 +274,7 @@ sub add { edited => 0x3fff, cancelled => $opt{cancelled} ? 1 : 0, route => JSON->new->encode( \@route ), + backend_id => $opt{backend_id}, }; if ( $opt{comment} ) { @@ -250,8 +307,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' } ) @@ -268,10 +331,11 @@ sub update { my $rows; my $journey = $self->get_single( - uid => $uid, - db => $db, - journey_id => $journey_id, - with_datetime => 1, + uid => $uid, + db => $db, + journey_id => $journey_id, + with_datetime => 1, + with_route_datetime => 1, ); eval { @@ -515,7 +579,7 @@ sub get { my @select = ( - qw(journey_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, @@ -573,12 +637,19 @@ sub get { my $ref = { id => $entry->{journey_id}, - type => $entry->{train_type}, + is_dbris => $entry->{is_dbris}, + is_iris => $entry->{is_iris}, + is_hafas => $entry->{is_hafas}, + is_motis => $entry->{is_motis}, + backend_name => $entry->{backend_name}, + backend_id => $entry->{backend_id}, + 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}, @@ -586,6 +657,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}, @@ -619,6 +691,14 @@ 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} ) { @@ -632,7 +712,10 @@ sub get { my $rename = $self->{renamed_station}; for my $stop ( @{ $ref->{route} } ) { if ( $stop->[0] =~ m{^Betriebsstelle nicht bekannt (\d+)$} ) { - if ( my $s = $self->{stations}->get_by_eva($1) ) { + if ( my $s + = $self->{stations} + ->get_by_eva( $1, backend_id => $ref->{backend_id} ) ) + { $stop->[0] = $s->{name}; } } @@ -767,14 +850,40 @@ sub get_oldest_ts { return undef; } -sub get_latest_checkout_station_id { +sub get_latest_checkout_latlon { + my ( $self, %opt ) = @_; + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + + my $res_h = $db->select( + 'journeys_str', + [ 'arr_lat', 'arr_lon', ], + { + user_id => $uid, + cancelled => 0 + }, + { + limit => 1, + order_by => { -desc => 'journey_id' } + } + )->hash; + + if ( not $res_h ) { + return; + } + + return $res_h->{arr_lat}, $res_h->{arr_lon}; + +} + +sub get_latest_checkout_ids { my ( $self, %opt ) = @_; my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; my $res_h = $db->select( 'journeys', - ['checkout_station_id'], + [ 'checkout_station_id', 'backend_id', ], { user_id => $uid, cancelled => 0 @@ -789,7 +898,7 @@ sub get_latest_checkout_station_id { return; } - return $res_h->{checkout_station_id}; + return $res_h->{checkout_station_id}, $res_h->{backend_id}; } sub get_latest_checkout_stations { @@ -800,7 +909,13 @@ sub get_latest_checkout_stations { my $res = $db->select( 'journeys_str', - [ 'arr_name', 'arr_eva', 'train_id' ], + [ + 'arr_name', 'arr_eva', + 'arr_external_id', 'train_id', + 'backend_id', 'backend_name', + 'is_dbris', 'is_efa', + 'is_hafas', 'is_motis' + ], { user_id => $uid, cancelled => 0 @@ -821,9 +936,15 @@ sub get_latest_checkout_stations { push( @ret, { - name => $row->{arr_name}, - eva => $row->{arr_eva}, - hafas => ( $row->{train_id} =~ m{[|]} ? 1 : 0 ), + name => $row->{arr_name}, + eva => $row->{arr_eva}, + external_id_or_eva => $row->{arr_external_id} + // $row->{arr_eva}, + dbris => $row->{is_dbris} ? $row->{backend_name} : 0, + efa => $row->{is_efa} ? $row->{backend_name} : 0, + hafas => $row->{is_hafas} ? $row->{backend_name} : 0, + motis => $row->{is_motis} ? $row->{backend_name} : 0, + backend_id => $row->{backend_id}, } ); } @@ -1021,32 +1142,33 @@ sub sanity_check { if ( defined $journey->{sched_duration} and $journey->{sched_duration} <= 0 ) { - return -'Die geplante Dauer dieser Fahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.'; + return 'Die geplante Dauer dieser Fahrt ist ≤ 0.' + . ' Teleportation und Zeitreisen werden in diesem Universum nicht unterstützt.'; } if ( defined $journey->{rt_duration} and $journey->{rt_duration} <= 0 ) { - return -'Die Dauer dieser Fahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.'; + return 'Die Dauer dieser Fahrt ist ≤ 0.' + . ' Teleportation und Zeitreisen werden in diesem Universum nicht unterstützt.'; } if ( $journey->{sched_duration} - and $journey->{sched_duration} > 60 * 60 * 24 ) + and $journey->{sched_duration} > 60 * 60 * 72 ) { - return 'Die Fahrt ist länger als 24 Stunden.'; + return 'Die Fahrt ist länger als drei Tage.'; } if ( $journey->{rt_duration} - and $journey->{rt_duration} > 60 * 60 * 24 ) + and $journey->{rt_duration} > 60 * 60 * 72 ) { - return 'Die Fahrt ist länger als 24 Stunden.'; + return 'Die Fahrt ist länger als drei Tage.'; } if ( $journey->{kmh_route} > 500 or $journey->{kmh_beeline} > 500 ) { - return 'Fahrten mit über 500 km/h? Schön wär\'s.'; + return 'Die berechnete Geschwindigkeit beträgt über 500 km/h.' + . ' Das wirkt unrealistisch.'; } if ( $journey->{route} and @{ $journey->{route} } > 199 ) { my $stop_count = @{ $journey->{route} }; - return -"Die Fahrt hat $stop_count Unterwegshalte. Also ich weiß ja nicht so recht."; + return "Die Fahrt hat $stop_count Unterwegshalte. " + . ' Stimmt das wirklich?'; } if ( $journey->{edited} & 0x0010 and not $lax ) { my @unknown_stations @@ -1082,19 +1204,62 @@ sub get_travel_distance { ->warn("Journey $journey->{id} has no from_name for EVA $from_eva"); } + # Work around inconsistencies caused by a multiple EVA IDs mapping to the same station name + if ( + @{ $polyline_ref // [] } + and not List::MoreUtils::any { $_->[2] and $_->[2] == $from_eva } + @{ $polyline_ref // [] } + ) + { + $self->{log}->debug( +"Journey $journey->{id} from_eva ($from_eva) is not part of polyline" + ); + for my $entry ( @{$route_ref} ) { + if ( $entry->[0] eq $from and $entry->[1] ) { + $from_eva = $entry->[1]; + $self->{log}->debug("... setting to $from_eva"); + last; + } + } + } + if ( + @{ $polyline_ref // [] } + and not List::MoreUtils::any { $_->[2] and $_->[2] == $to_eva } + @{ $polyline_ref // [] } + ) + { + $self->{log}->debug( + "Journey $journey->{id} to_eva ($to_eva) is not part of polyline"); + for my $entry ( @{$route_ref} ) { + if ( $entry->[0] eq $to and $entry->[1] ) { + $to_eva = $entry->[1]; + $self->{log}->debug("... setting to $to_eva"); + last; + } + } + } + my $distance_polyline = 0; my $distance_intermediate = 0; - my $distance_beeline = 0; - my $skipped = 0; my $geo = GIS::Distance->new(); - my @stations = map { $_->[0] } @{$route_ref}; - my @route = after_incl { $_ eq $from } @stations; - @route = before_incl { $_ eq $to } @route; + my $distance_beeline + = $geo->distance_metal( @{$from_latlon}, @{$to_latlon} ); + my @route + = after_incl { ( $_->[1] and $_->[1] == $from_eva ) or $_->[0] eq $from } + @{$route_ref}; + @route + = before_incl { ( $_->[1] and $_->[1] == $to_eva ) or $_->[0] eq $to } + @route; - if ( @route < 2 ) { + if ( + @route < 2 + or ( $route[-1][0] ne $to + and ( not $route[-1][1] or $route[-1][1] != $to_eva ) ) + ) + { # I AM ERROR - return ( 0, 0, 0 ); + return ( 0, 0, $distance_beeline ); } my @polyline = after_incl { $_->[2] and $_->[2] == $from_eva } @@ -1102,34 +1267,32 @@ sub get_travel_distance { @polyline = before_incl { $_->[2] and $_->[2] == $to_eva } @polyline; - my $prev_station = shift @polyline; - for my $station (@polyline) { - $distance_polyline += $geo->distance_metal( - $prev_station->[1], $prev_station->[0], - $station->[1], $station->[0] - ); - $prev_station = $station; - } - - $prev_station = $self->{latlon_by_station}->{ shift @route }; - if ( not $prev_station ) { - return ( $distance_polyline, 0, 0 ); - } - - for my $station_name (@route) { - if ( my $station = $self->{latlon_by_station}->{$station_name} ) { - $distance_intermediate += $geo->distance_metal( - $prev_station->[0], $prev_station->[1], - $station->[0], $station->[1] + # ensure that before_incl matched -- otherwise, @polyline is too long + if ( @polyline and $polyline[-1][2] == $to_eva ) { + my $prev_station = shift @polyline; + for my $station (@polyline) { + $distance_polyline += $geo->distance_metal( + $prev_station->[1], $prev_station->[0], + $station->[1], $station->[0] ); $prev_station = $station; } } - $distance_beeline = $geo->distance_metal( @{$from_latlon}, @{$to_latlon} ); + if ( defined $route[0][2]{lat} and defined $route[0][2]{lon} ) { + my $prev_station = shift @route; + for my $station (@route) { + if ( defined $station->[2]{lat} and defined $station->[2]{lon} ) { + $distance_intermediate += $geo->distance_metal( + $prev_station->[2]{lat}, $prev_station->[2]{lon}, + $station->[2]{lat}, $station->[2]{lon} + ); + $prev_station = $station; + } + } + } - return ( $distance_polyline, $distance_intermediate, - $distance_beeline, $skipped ); + return ( $distance_polyline, $distance_intermediate, $distance_beeline ); } sub grep_single { @@ -1548,7 +1711,10 @@ sub compute_stats { @inconsistencies, { conflict => { - train => $journey->{type} . ' ' + train => ( + $journey->{is_motis} ? '' : $journey->{type} + ) + . ' ' . ( $journey->{line} // $journey->{no} ), arr => epoch_to_dt( $journey->{rt_arr_ts} ) ->strftime('%d.%m.%Y %H:%M'), @@ -1574,7 +1740,8 @@ sub compute_stats { $next_departure = $journey->{rt_dep_ts}; $next_id = $journey->{id}; $next_train - = $journey->{type} . ' ' . ( $journey->{line} // $journey->{no} ),; + = ( $journey->{is_motis} ? '' : $journey->{type} ) . ' ' + . ( $journey->{line} // $journey->{no} ),; } my $ret = { km_route => $km_route, @@ -1607,6 +1774,8 @@ sub compute_stats { sub get_stats { my ( $self, %opt ) = @_; + $self->{log}->debug("get_stats"); + if ( $opt{cancelled} ) { $self->{log} ->warn('get_journey_stats called with illegal option cancelled => 1'); @@ -1633,9 +1802,12 @@ sub get_stats { ) ) { + $self->{log}->debug("got cached journey stats for $year/$month"); return $stats; } + $self->{log}->debug("computing journey stats for $year/$month"); + my $interval_start = DateTime->new( time_zone => 'Europe/Berlin', year => 2000, @@ -1694,28 +1866,29 @@ sub get_stats { return $stats; } -sub get_latest_dest_id { +sub get_latest_dest_ids { my ( $self, %opt ) = @_; my $uid = $opt{uid}; my $db = $opt{db} // $self->{pg}->db; if ( - my $id = $self->{in_transit}->get_checkout_station_id( + my ( $id, $backend_id ) = $self->{in_transit}->get_checkout_ids( uid => $uid, db => $db ) ) { - return $id; + return ( $id, $backend_id ); } - return $self->get_latest_checkout_station_id( + return $self->get_latest_checkout_ids( uid => $uid, db => $db ); } +# Returns a listref of {eva, name} hashrefs for the specified backend. sub get_connection_targets { my ( $self, %opt ) = @_; @@ -1724,21 +1897,32 @@ sub get_connection_targets { // 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}; if ( $opt{destination_name} ) { - return ( - [], - [ { eva => $opt{eva}, name => $opt{destination_name} } ] - ); + return { + eva => $opt{eva}, + name => $opt{destination_name} + }; } - my $dest_id = $opt{eva} // $self->get_latest_dest_id(%opt); + my $backend_id = $opt{backend_id}; if ( not $dest_id ) { - return ( [], [] ); + ( $dest_id, $backend_id ) = $self->get_latest_dest_ids(%opt); } - my $dest_ids = [ $dest_id, $self->{stations}->get_meta( eva => $dest_id ) ]; + if ( not $dest_id ) { + return; + } + + my $dest_ids = [ + $dest_id, + $self->{stations}->get_meta( + eva => $dest_id, + backend_id => $backend_id, + ) + ]; my $res = $db->select( 'journeys', @@ -1746,7 +1930,8 @@ sub get_connection_targets { { user_id => $uid, checkin_station_id => $dest_ids, - real_departure => { '>', $threshold } + real_departure => { '>', $threshold }, + backend_id => $opt{backend_id}, }, { group_by => ['checkout_station_id'], @@ -1755,9 +1940,13 @@ sub get_connection_targets { ); my @destinations = $res->hashes->grep( sub { shift->{count} >= $min_count } ) - ->map( sub { shift->{dest} } )->each; - @destinations = $self->{stations}->get_by_evas(@destinations); - return ( $dest_ids, \@destinations ); + ->map( sub { shift->{dest} } ) + ->each; + @destinations = $self->{stations}->get_by_evas( + backend_id => $opt{backend_id}, + evas => [@destinations] + ); + return @destinations; } sub update_visibility { diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm index ac4019c..c6d9730 100644 --- a/lib/Travelynx/Model/Stations.pm +++ b/lib/Travelynx/Model/Stations.pm @@ -1,6 +1,7 @@ package Travelynx::Model::Stations; # Copyright (C) 2022 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> # # SPDX-License-Identifier: AGPL-3.0-or-later @@ -14,56 +15,319 @@ sub new { return bless( \%opt, $class ); } +sub get_backend_id { + my ( $self, %opt ) = @_; + + if ( $opt{iris} ) { + + # special case + return 0; + } + if ( $opt{dbris} and $self->{backend_id}{dbris}{ $opt{dbris} } ) { + return $self->{backend_id}{dbris}{ $opt{dbris} }; + } + if ( $opt{efa} and $self->{backend_id}{efa}{ $opt{efa} } ) { + return $self->{backend_id}{efa}{ $opt{efa} }; + } + if ( $opt{hafas} and $self->{backend_id}{hafas}{ $opt{hafas} } ) { + return $self->{backend_id}{hafas}{ $opt{hafas} }; + } + if ( $opt{motis} and $self->{backend_id}{motis}{ $opt{motis} } ) { + return $self->{backend_id}{motis}{ $opt{motis} }; + } + + my $db = $opt{db} // $self->{pg}->db; + my $backend_id = 0; + + if ( $opt{dbris} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + dbris => 1, + name => $opt{dbris} + } + )->hash->{id}; + $self->{backend_id}{dbris}{ $opt{dbris} } = $backend_id; + } + elsif ( $opt{efa} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + efa => 1, + name => $opt{efa} + } + )->hash->{id}; + $self->{backend_id}{efa}{ $opt{efa} } = $backend_id; + } + elsif ( $opt{hafas} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + hafas => 1, + name => $opt{hafas} + } + )->hash->{id}; + $self->{backend_id}{hafas}{ $opt{hafas} } = $backend_id; + } + elsif ( $opt{motis} ) { + $backend_id = $db->select( + 'backends', + ['id'], + { + motis => 1, + name => $opt{motis} + } + )->hash->{id}; + $self->{backend_id}{motis}{ $opt{motis} } = $backend_id; + } + + return $backend_id; +} + +sub get_backend { + my ( $self, %opt ) = @_; + + if ( $self->{backend_cache}{ $opt{backend_id} } ) { + return $self->{backend_cache}{ $opt{backend_id} }; + } + + my $db = $opt{db} // $self->{pg}->db; + my $ret = $db->select( + 'backends', + '*', + { + id => $opt{backend_id}, + } + )->hash; + + $self->{backend_cache}{ $opt{backend_id} } = $ret; + + return $ret; +} + +sub get_backends { + my ( $self, %opt ) = @_; + + $opt{db} //= $self->{pg}->db; + + my $res = $opt{db}->select( 'backends', + [ 'id', 'name', 'dbris', 'efa', 'hafas', 'iris', 'motis' ] ); + my @ret; + + while ( my $row = $res->hash ) { + push( + @ret, + { + id => $row->{id}, + name => $row->{name}, + dbris => $row->{dbris}, + efa => $row->{efa}, + hafas => $row->{hafas}, + iris => $row->{iris}, + motis => $row->{motis}, + } + ); + } + + return @ret; +} + +# Slow for MOTIS backends sub add_or_update { my ( $self, %opt ) = @_; - my $stop = $opt{stop}; - my $loc = $stop->loc; - my $source = 1; - my $db = $opt{db} // $self->{pg}->db; + my $stop = $opt{stop}; + $opt{db} //= $self->{pg}->db; + + $opt{backend_id} //= $self->get_backend_id(%opt); + + if ( $opt{dbris} ) { + if ( + my $s = $self->get_by_eva( + $stop->eva, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( + 'stations', + { + name => $stop->name, + lat => $stop->lat, + lon => $stop->lon, + archived => 0 + }, + { + eva => $stop->eva, + source => $opt{backend_id} + } + ); + return; + } + $opt{db}->insert( + 'stations', + { + eva => $stop->eva, + name => $stop->name, + lat => $stop->lat, + lon => $stop->lon, + source => $opt{backend_id}, + archived => 0 + } + ); + return; + } + + if ( $opt{efa} ) { + if ( + my $s = $self->get_by_eva( + $stop->id_num, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( + 'stations', + { + name => $stop->full_name, + lat => $stop->latlon->[0], + lon => $stop->latlon->[1], + archived => 0 + }, + { + eva => $stop->id_num, + source => $opt{backend_id} + } + ); + return; + } + if (not $stop->latlon) { + die('Backend Error: Stop "' . $stop->full_name . '" has no geo coordinates'); + } + $opt{db}->insert( + 'stations', + { + eva => $stop->id_num, + name => $stop->full_name, + lat => $stop->latlon->[0], + lon => $stop->latlon->[1], + source => $opt{backend_id}, + archived => 0 + } + ); + return; + } + + if ( $opt{motis} ) { + if ( + my $s = $self->get_by_external_id( + external_id => $stop->id, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( + 'stations', + { + name => $stop->name, + lat => $stop->lat, + lon => $stop->lon, + archived => 0 + }, + { + eva => $s->{eva}, + source => $opt{backend_id} + } + ); - if ( my $s = $self->get_by_eva( $loc->eva, db => $db ) ) { - if ( $source == 1 and $s->{source} == 0 and not $s->{archived} ) { + # MOTIS backends do not provide a numeric ID, so we set our ID here. + $stop->{eva} = $s->{eva}; return; } - $db->update( + + my $s = $opt{db}->query( + qq { + with new_station as ( + insert into stations_external_ids (backend_id, external_id) + values (?, ?) + returning eva, backend_id + ) + + insert into stations (eva, name, lat, lon, source, archived) + values ((select eva from new_station), ?, ?, ?, (select backend_id from new_station), ?) + returning * + }, + ( + $opt{backend_id}, $stop->id, $stop->name, + $stop->lat, $stop->lon, 0, + ) + ); + + # MOTIS backends do not provide a numeric ID, so we set our ID here. + $stop->{eva} = $s->hash->{eva}; + return; + } + + my $loc = $stop->loc; + if ( + my $s = $self->get_by_eva( + $loc->eva, + db => $opt{db}, + backend_id => $opt{backend_id} + ) + ) + { + $opt{db}->update( 'stations', { name => $loc->name, lat => $loc->lat, lon => $loc->lon, - source => $source, archived => 0 }, - { eva => $loc->eva } + { + eva => $loc->eva, + source => $opt{backend_id} + } ); return; } - $db->insert( + $opt{db}->insert( 'stations', { eva => $loc->eva, name => $loc->name, lat => $loc->lat, lon => $loc->lon, - source => $source, + source => $opt{backend_id}, archived => 0 } ); + + return; } sub add_meta { my ( $self, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; my $eva = $opt{eva}; my @meta = @{ $opt{meta} }; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); + for my $meta (@meta) { if ( $meta != $eva ) { - $db->insert( + $opt{db}->insert( 'related_stations', { - eva => $eva, - meta => $meta + eva => $eva, + meta => $meta, + backend_id => $opt{backend_id}, }, { on_conflict => undef } ); @@ -74,7 +338,7 @@ sub add_meta { sub get_db_iterator { my ($self) = @_; - return $self->{pg}->db->select( 'stations', '*' ); + return $self->{pg}->db->select( 'stations_str', '*' ); } sub get_meta { @@ -82,7 +346,16 @@ sub get_meta { my $db = $opt{db} // $self->{pg}->db; my $eva = $opt{eva}; - my $res = $db->select( 'related_stations', ['meta'], { eva => $eva } ); + $opt{backend_id} //= $self->get_backend_id( %opt, db => $db ); + + my $res = $db->select( + 'related_stations', + ['meta'], + { + eva => $eva, + backend_id => $opt{backend_id} + } + ); my @ret; while ( my $row = $res->hash ) { @@ -93,9 +366,12 @@ sub get_meta { } sub get_for_autocomplete { - my ($self) = @_; + my ( $self, %opt ) = @_; - my $res = $self->{pg}->db->select( 'stations', ['name'] ); + $opt{backend_id} //= $self->get_backend_id(%opt); + + my $res = $self->{pg} + ->db->select( 'stations', ['name'], { source => $opt{backend_id} } ); my %ret; while ( my $row = $res->hash ) { @@ -113,43 +389,74 @@ sub get_by_eva { return; } - my $db = $opt{db} // $self->{pg}->db; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); - return $db->select( 'stations', '*', { eva => $eva } )->hash; + return $opt{db}->select( + 'stations', + '*', + { + eva => $eva, + source => $opt{backend_id} + } + )->hash; } -# Fast -sub get_by_evas { - my ( $self, @evas ) = @_; +# Slow +sub get_by_external_id { + my ( $self, %opt ) = @_; - my @ret - = $self->{pg}->db->select( 'stations', '*', { eva => { '=', \@evas } } ) - ->hashes->each; - return @ret; + if ( not $opt{external_id} ) { + return; + } + + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); + + return $opt{db}->select( + 'stations_with_external_ids', + '*', + { + external_id => $opt{external_id}, + source => $opt{backend_id}, + } + )->hash; } -# Slow -sub get_latlon_by_name { +# Fast +sub get_by_evas { my ( $self, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); - my %location; - my $res = $db->select( 'stations', [ 'name', 'lat', 'lon' ] ); - while ( my $row = $res->hash ) { - $location{ $row->{name} } = [ $row->{lat}, $row->{lon} ]; - } - return \%location; + my @ret = $self->{pg}->db->select( + 'stations', + '*', + { + eva => { '=', $opt{evas} }, + source => $opt{backend_id} + } + )->hashes->each; + return @ret; } # Slow sub get_by_name { my ( $self, $name, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); - return $db->select( 'stations', '*', { name => $name }, { limit => 1 } ) - ->hash; + return $opt{db}->select( + 'stations', + '*', + { + name => $name, + source => $opt{backend_id} + }, + { limit => 1 } + )->hash; } # Slow @@ -166,16 +473,27 @@ sub get_by_names { sub get_by_ds100 { my ( $self, $ds100, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); - return $db->select( 'stations', '*', { ds100 => $ds100 }, { limit => 1 } ) - ->hash; + return $opt{db}->select( + 'stations', + '*', + { + ds100 => $ds100, + source => $opt{backend_id} + }, + { limit => 1 } + )->hash; } # Can be slow sub search { my ( $self, $identifier, %opt ) = @_; + $opt{db} //= $self->{pg}->db; + $opt{backend_id} //= $self->get_backend_id(%opt); + if ( $identifier =~ m{ ^ \d+ $ }x ) { return $self->get_by_eva( $identifier, %opt ) // $self->get_by_ds100( $identifier, %opt ) diff --git a/lib/Travelynx/Model/Traewelling.pm b/lib/Travelynx/Model/Traewelling.pm index 25648cc..608da15 100644 --- a/lib/Travelynx/Model/Traewelling.pm +++ b/lib/Travelynx/Model/Traewelling.pm @@ -224,6 +224,7 @@ sub get_pushable_accounts { join in_transit_str as i on t.user_id = i.user_id where t.push_sync = True and i.arr_eva is not null + and i.backend_id = (select id from backends where dbris = true and name = 'bahn.de') and i.cancelled = False } ); diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index 4602fa2..be9e80b 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -40,14 +40,6 @@ my %predicate_atoi = ( is_blocked_by => 3, ); -my @sb_templates = ( - undef, - [ 'DBF', 'https://dbf.finalrewind.org/{name}?rt=1#{tt}{tn}' ], - [ 'bahn.expert', 'https://bahn.expert/{name}#{id}' ], - [ 'DBF HAFAS', 'https://dbf.finalrewind.org/{name}?rt=1&hafas=1#{tt}{tn}' ], - [ 'bahn.expert/regional', 'https://bahn.expert/regional/{name}#{id}' ], -); - my %token_id = ( status => 1, history => 2, @@ -213,6 +205,17 @@ sub get_privacy_by { return; } +sub set_backend { + my ( $self, %opt ) = @_; + $opt{db} //= $self->{pg}->db; + + $opt{db}->update( + 'users', + { backend_id => $opt{backend_id} }, + { id => $opt{uid} } + ); +} + sub set_privacy { my ( $self, %opt ) = @_; my $db = $opt{db} // $self->{pg}->db; @@ -409,12 +412,13 @@ sub get { my $uid = $opt{uid}; my $user = $db->select( - 'users', + 'users_with_backend', 'id, name, status, public_level, email, ' - . 'external_services, accept_follows, notifications, ' + . 'accept_follows, notifications, ' . 'extract(epoch from registered_at) as registered_at_ts, ' . 'extract(epoch from last_seen) as last_seen_ts, ' - . 'extract(epoch from deletion_requested) as deletion_requested_ts', + . 'extract(epoch from deletion_requested) as deletion_requested_ts, ' + . 'backend_id, backend_name, dbris, efa, hafas, motis', { id => $uid } )->hash; if ($user) { @@ -435,12 +439,8 @@ sub get { past_status => $user->{public_level} & 0x08000 ? 1 : 0, past_all => $user->{public_level} & 0x10000 ? 1 : 0, email => $user->{email}, - sb_name => $user->{external_services} - ? $sb_templates[ $user->{external_services} & 0x07 ][0] - : undef, - sb_template => $user->{external_services} - ? $sb_templates[ $user->{external_services} & 0x07 ][1] - : undef, + sb_template => +'https://dbf.finalrewind.org/{name}?dbris={dbris}&efa={efa}&hafas={hafas}&motis={motis}#{id_or_tttn}', registered_at => DateTime->from_epoch( epoch => $user->{registered_at_ts}, time_zone => 'Europe/Berlin' @@ -455,6 +455,12 @@ sub get { time_zone => 'Europe/Berlin' ) : undef, + backend_id => $user->{backend_id}, + backend_name => $user->{backend_name}, + backend_dbris => $user->{dbris}, + backend_efa => $user->{efa}, + backend_hafas => $user->{hafas}, + backend_motis => $user->{motis}, }; } return undef; @@ -659,24 +665,6 @@ sub use_history { } } -sub use_external_services { - my ( $self, %opt ) = @_; - my $db = $opt{db} // $self->{pg}->db; - my $uid = $opt{uid}; - my $value = $opt{set}; - - if ( defined $value ) { - if ( $value < 0 or $value > 4 ) { - $value = 0; - } - $db->update( 'users', { external_services => $value }, { id => $uid } ); - } - else { - return $db->select( 'users', ['external_services'], { id => $uid } ) - ->hash->{external_services}; - } -} - sub get_webhook { my ( $self, %opt ) = @_; my $db = $opt{db} // $self->{pg}->db; |