From 9710189897448f3f4a144e27325578c900250068 Mon Sep 17 00:00:00 2001 From: Daniel Friesel Date: Sun, 28 Jun 2020 08:59:40 +0200 Subject: add experimental (and unreferenced) train intersection calculation --- lib/DBInfoscreen.pm | 1 + lib/DBInfoscreen/Controller/Map.pm | 427 +++++++++++++++++++++++++++++--- templates/_intersection_infobox.html.ep | 22 ++ templates/route_map.html.ep | 5 +- 4 files changed, 419 insertions(+), 36 deletions(-) create mode 100644 templates/_intersection_infobox.html.ep diff --git a/lib/DBInfoscreen.pm b/lib/DBInfoscreen.pm index e49ce70..83b7103 100644 --- a/lib/DBInfoscreen.pm +++ b/lib/DBInfoscreen.pm @@ -317,6 +317,7 @@ sub startup { $r->get('/_ajax_mapinfo/:tripid/:lineno')->to('map#ajax_route'); $r->get('/map/:tripid/:lineno')->to('map#route'); + $r->get('/intersection/:trips')->to('map#intersection'); $self->defaults( layout => 'app' ); diff --git a/lib/DBInfoscreen/Controller/Map.pm b/lib/DBInfoscreen/Controller/Map.pm index 91aefe6..4497a4a 100644 --- a/lib/DBInfoscreen/Controller/Map.pm +++ b/lib/DBInfoscreen/Controller/Map.pm @@ -7,6 +7,7 @@ use Mojo::Promise; use DateTime; use DateTime::Format::Strptime; use Geo::Distance; +use List::Util qw(); my $dbf_version = qx{git describe --dirty} || 'experimental'; @@ -75,6 +76,110 @@ sub get_hafas_polyline_p { return $promise; } +sub get_route_indexes { + my ( $features, $from_name, $to_name ) = @_; + my ( $from_index, $to_index ); + + for my $i ( 0 .. $#{$features} ) { + my $this_point = $features->[$i]; + if ( not defined $from_index + and $this_point->{properties}{type} + and $this_point->{properties}{type} eq 'stop' + and $this_point->{properties}{name} eq $from_name ) + { + $from_index = $i; + } + elsif ( $this_point->{properties}{type} + and $this_point->{properties}{type} eq 'stop' + and $this_point->{properties}{name} eq $to_name ) + { + $to_index = $i; + last; + } + } + return ( $from_index, $to_index ); +} + +# Returns timestamped train positions between stop1 and stop2 (must not have +# intermittent stops) in 10-second steps. +sub estimate_timestamped_positions { + my (%opt) = @_; + + my $from_dt = $opt{from}{dep}; + my $to_dt = $opt{to}{arr}; + my $from_name = $opt{from}{name}; + my $to_name = $opt{to}{name}; + my $features = $opt{features}; + + my $duration = $to_dt->epoch - $from_dt->epoch; + + my @train_positions; + + my @completion_ratios + = map { ( $_ * 10 / $duration ) } ( 0 .. $duration / 10 ); + + my ( $from_index, $to_index ) + = get_route_indexes( $features, $from_name, $to_name ); + + my $location_epoch = $from_dt->epoch; + my $geo = Geo::Distance->new; + + if ( defined $from_index and defined $to_index ) { + my $total_distance = 0; + for my $j ( $from_index + 1 .. $to_index ) { + my $prev = $features->[ $j - 1 ]{geometry}{coordinates}; + my $this = $features->[$j]{geometry}{coordinates}; + if ( $prev and $this ) { + $total_distance += $geo->distance( + 'kilometer', $prev->[0], $prev->[1], + $this->[0], $this->[1] + ); + } + } + my @marker_distances = map { $total_distance * $_ } @completion_ratios; + $total_distance = 0; + for my $j ( $from_index + 1 .. $to_index ) { + my $prev = $features->[ $j - 1 ]{geometry}{coordinates}; + my $this = $features->[$j]{geometry}{coordinates}; + if ( $prev and $this ) { + my $prev_distance = $total_distance; + $total_distance += $geo->distance( + 'kilometer', $prev->[0], $prev->[1], + $this->[0], $this->[1] + ); + for my $i ( @train_positions .. $#marker_distances ) { + my $marker_distance = $marker_distances[$i]; + if ( $total_distance > $marker_distance ) { + + # completion ratio for the line between (prev, this) + my $sub_ratio = 1; + if ( $total_distance != $prev_distance ) { + $sub_ratio = ( $marker_distance - $prev_distance ) + / ( $total_distance - $prev_distance ); + } + + my $lat = $prev->[1] + + ( $this->[1] - $prev->[1] ) * $sub_ratio; + my $lon = $prev->[0] + + ( $this->[0] - $prev->[0] ) * $sub_ratio; + + push( @train_positions, + [ $location_epoch, $lat, $lon ] ); + $location_epoch += 10; + } + } + if ( @train_positions == @completion_ratios ) { + return @train_positions; + } + } + } + if (@train_positions) { + return @train_positions; + } + } + return; +} + # Input: # now: DateTime # from: current/previous stop @@ -107,25 +212,10 @@ sub estimate_train_positions { = map { ( $time_complete + ( $_ * 2 ) ) / $time_total } ( 0 .. 45 ); my $geo = Geo::Distance->new; - my ( $from_index, $to_index ); - for my $j ( 0 .. $#{$features} ) { - my $this_point = $features->[$j]; - if ( not defined $from_index - and $this_point->{properties}{type} - and $this_point->{properties}{type} eq 'stop' - and $this_point->{properties}{name} eq $from_name ) - { - $from_index = $j; - } - elsif ( $this_point->{properties}{type} - and $this_point->{properties}{type} eq 'stop' - and $this_point->{properties}{name} eq $to_name ) - { - $to_index = $j; - last; - } - } + my ( $from_index, $to_index ) + = get_route_indexes( $features, $from_name, $to_name ); + if ( defined $from_index and defined $to_index ) { my $total_distance = 0; for my $j ( $from_index + 1 .. $to_index ) { @@ -259,6 +349,145 @@ sub estimate_train_positions2 { }; } +sub estimate_train_intersection { + my (%opt) = @_; + my @route1 = @{ $opt{routes}[0] // [] }; + my @route2 = @{ $opt{routes}[1] // [] }; + + my $ret; + + my $i1 = 0; + my $i2 = 0; + + my @pairs; + my @meeting_points; + my $geo = Geo::Distance->new; + + # skip last route element as we compare route[i] with route[i+1] + while ( $i1 < $#route1 and $i2 < $#route2 ) { + my $dep1 = $route1[$i1]{dep}; + my $arr1 = $route1[ $i1 + 1 ]{arr}; + my $dep2 = $route2[$i2]{dep}; + my $arr2 = $route2[ $i2 + 1 ]{arr}; + + if ( not( $dep1 and $arr1 ) ) { + say "skip 1 $route1[$i1]{name}"; + $i1++; + next; + } + + if ( not( $dep2 and $arr2 ) ) { + say "skip 2 $route2[$i2]{name}"; + $i2++; + next; + } + + if ( $arr1 <= $dep2 ) { + $i1++; + } + elsif ( $arr2 <= $dep1 ) { + $i2++; + } + elsif ( $arr2 <= $arr1 ) { + push( @pairs, [ $i1, $i2 ] ); + if ( $route1[$i1]{name} eq $route2[ $i2 + 1 ]{name} + and $route2[$i2]{name} eq $route1[ $i1 + 1 ]{name} ) + { + # both i1 name == i2+1 name and i1 name == i2 name are valid cases + # (trains don't just intersect when they travel in opposing + # directions -- they may also travel in the same direction + # with different speed and overtake each other). + # We need both stop pairs later on, so we save both. + $ret->{stop_pair} = [ + [ $route1[$i1]{name}, $route1[ $i1 + 1 ]{name} ], + [ $route2[$i2]{name}, $route2[ $i2 + 1 ]{name} ] + ]; + } + $i2++; + } + elsif ( $arr1 <= $arr2 ) { + push( @pairs, [ $i1, $i2 ] ); + if ( $route1[$i1]{name} eq $route2[ $i2 + 1 ]{name} + and $route2[$i2]{name} eq $route1[ $i1 + 1 ]{name} ) + { + $ret->{stop_pair} = [ + [ $route1[$i1]{name}, $route1[ $i1 + 1 ]{name} ], + [ $route2[$i2]{name}, $route2[ $i2 + 1 ]{name} ] + ]; + } + $i1++; + } + else { + $i1++; + } + } + + for my $pair (@pairs) { + my ( $i1, $i2 ) = @{$pair}; + my @train1_positions = estimate_timestamped_positions( + from => $route1[$i1], + to => $route1[ $i1 + 1 ], + features => $opt{features}[0], + ); + my @train2_positions = estimate_timestamped_positions( + from => $route2[$i2], + to => $route2[ $i2 + 1 ], + features => $opt{features}[1], + ); + $i1 = 0; + $i2 = 0; + while ( $i1 <= $#train1_positions and $i2 <= $#train2_positions ) { + if ( $train1_positions[$i1][0] < $train2_positions[$i2][0] ) { + $i1++; + } + elsif ( $train1_positions[$i2][0] < $train2_positions[$i2][0] ) { + $i2++; + } + else { + if ( + ( + my $distance = $geo->distance( + 'kilometer', + $train1_positions[$i1][2], + $train1_positions[$i1][1], + $train2_positions[$i2][2], + $train2_positions[$i2][1] + ) + ) < 1 + ) + { + my $ts = DateTime->from_epoch( + epoch => $train1_positions[$i1][0], + time_zone => 'Europe/Berlin' + ); + $ret->{first_meeting_time} //= $ts; + push( + @meeting_points, + { + timestamp => $ts, + lat => ( + $train1_positions[$i1][1] + + $train2_positions[$i2][1] + ) / 2, + lon => ( + $train1_positions[$i1][2] + + $train2_positions[$i2][2] + ) / 2, + distance => $distance, + } + ); + } + $i1++; + $i2++; + } + } + } + + $ret->{meeting_points} = \@meeting_points; + + return $ret; +} + sub route_to_ajax { my (@stopovers) = @_; @@ -348,6 +577,145 @@ sub stopovers_to_route { return @route; } +sub polyline_to_line_pairs { + my (@polyline) = @_; + my @line_pairs; + for my $i ( 1 .. $#polyline ) { + push( + @line_pairs, + [ + [ $polyline[ $i - 1 ][1], $polyline[ $i - 1 ][0] ], + [ $polyline[$i][1], $polyline[$i][0] ] + ] + ); + } + return @line_pairs; +} + +sub intersection { + my ($self) = @_; + + my @trips = split( qr{;}, $self->stash('trips') ); + my @trip_ids = map { [ split( qr{,}, $_ ) ] } @trips; + + $self->render_later; + + my @polyline_requests + = map { $self->get_hafas_polyline_p( @{$_} ) } @trip_ids; + Mojo::Promise->all(@polyline_requests)->then( + sub { + my ( $pl1, $pl2 ) = map { $_->[0] } @_; + my @polyline1 = @{ $pl1->{polyline} }; + my @polyline2 = @{ $pl2->{polyline} }; + my @station_coordinates; + + my @markers; + my $next_stop; + + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + + my @line1_pairs = polyline_to_line_pairs(@polyline1); + my @line2_pairs = polyline_to_line_pairs(@polyline2); + + my @route1 + = stopovers_to_route( @{ $pl1->{raw}{stopovers} // [] } ); + my @route2 + = stopovers_to_route( @{ $pl2->{raw}{stopovers} // [] } ); + + my $train1_pos = estimate_train_positions2( + now => $now, + route => \@route1, + features => $pl1->{raw}{polyline}{features}, + ); + + my $train2_pos = estimate_train_positions2( + now => $now, + route => \@route2, + features => $pl2->{raw}{polyline}{features}, + ); + + my $intersection = estimate_train_intersection( + routes => [ \@route1, \@route2 ], + features => [ + $pl1->{raw}{polyline}{features}, + $pl2->{raw}{polyline}{features} + ], + ); + + for my $meeting_point ( @{ $intersection->{meeting_points} } ) { + push( + @station_coordinates, + [ + [ $meeting_point->{lat}, $meeting_point->{lon} ], + [ $meeting_point->{timestamp}->strftime('%H:%M') ] + ] + ); + } + + push( + @markers, + { + lat => $train1_pos->{position_now}[0], + lon => $train1_pos->{position_now}[1], + title => $pl1->{name} + }, + { + lat => $train2_pos->{position_now}[0], + lon => $train2_pos->{position_now}[1], + title => $pl2->{name} + }, + ); + + $self->render( + 'route_map', + title => "DBF", + hide_opts => 1, + with_map => 1, + intersection => 1, + train1_no => + scalar( $pl1->{raw}{line}{additionalName} // $pl1->{name} ), + train2_no => + scalar( $pl2->{raw}{line}{additionalName} // $pl2->{name} ), + likely_pair => $intersection->{stop_pair} + ? $intersection->{stop_pair}[0] + : undef, + time => scalar $intersection->{first_meeting_time}, + polyline_groups => [ + { + polylines => [ @line1_pairs, @line2_pairs ], + color => '#ffffff', + opacity => 0, + fit_bounds => 1, + }, + { + polylines => [@line1_pairs], + color => '#005080', + opacity => 0.6, + }, + { + polylines => [@line2_pairs], + color => '#800050', + opacity => 0.6, + } + ], + markers => [@markers], + station_coordinates => [@station_coordinates], + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + 'route_map', + title => "DBF", + hide_opts => 1, + with_map => 1, + error => $err, + ); + } + )->wait; +} + sub route { my ($self) = @_; my $trip_id = $self->stash('tripid'); @@ -363,7 +731,6 @@ sub route { my ($pl) = @_; my @polyline = @{ $pl->{polyline} }; - my @line_pairs; my @station_coordinates; my @markers; @@ -371,16 +738,8 @@ sub route { my $now = DateTime->now( time_zone => 'Europe/Berlin' ); - # @line_pairs are used to draw the train's journey on the map - for my $i ( 1 .. $#polyline ) { - push( - @line_pairs, - [ - [ $polyline[ $i - 1 ][1], $polyline[ $i - 1 ][0] ], - [ $polyline[$i][1], $polyline[$i][0] ] - ] - ); - } + # used to draw the train's journey on the map + my @line_pairs = polyline_to_line_pairs(@polyline); my @route = stopovers_to_route( @{ $pl->{raw}{stopovers} // [] } ); @@ -490,12 +849,10 @@ sub route { my ($err) = @_; $self->render( 'route_map', - title => "DBF", - hide_opts => 1, - with_map => 1, - error => $err, - origin => undef, - destination => undef, + title => "DBF", + hide_opts => 1, + with_map => 1, + error => $err, ); } diff --git a/templates/_intersection_infobox.html.ep b/templates/_intersection_infobox.html.ep new file mode 100644 index 0000000..cb27d19 --- /dev/null +++ b/templates/_intersection_infobox.html.ep @@ -0,0 +1,22 @@ +
+
+ <%= stash('train1_no') %> + und + <%= stash('train2_no') %> + werden sich wahrscheinlich + % if (my $t = stash('time')) { + gegen <%= $t->strftime('%H:%M') %> Uhr + % } + % if (my $p = stash('likely_pair')) { + zwischen <%= $p->[0] %> und <%= $p->[1] %> + % } + % if (not stash('time')) { + nicht + % } + begegnen. +
+
diff --git a/templates/route_map.html.ep b/templates/route_map.html.ep index 6f5643b..1ba96bc 100644 --- a/templates/route_map.html.ep +++ b/templates/route_map.html.ep @@ -1,6 +1,9 @@ -% if ($origin and $destination) { +% if (stash('origin') and stash('destination')) { %= include '_map_infobox' % } +% elsif (stash('intersection')) { + %= include '_intersection_infobox' +% }
-- cgit v1.2.3