summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Friesel <derf@finalrewind.org>2020-06-28 08:59:40 +0200
committerDaniel Friesel <derf@finalrewind.org>2020-06-28 08:59:40 +0200
commit9710189897448f3f4a144e27325578c900250068 (patch)
treef6cdb78b602e6f9e1f84f3a1c2232c184df05a6f
parent4587af307d0c5fc8b53d082f346fc5f5ba042168 (diff)
add experimental (and unreferenced) train intersection calculation
-rw-r--r--lib/DBInfoscreen.pm1
-rw-r--r--lib/DBInfoscreen/Controller/Map.pm427
-rw-r--r--templates/_intersection_infobox.html.ep22
-rw-r--r--templates/route_map.html.ep5
4 files changed, 419 insertions, 36 deletions
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 @@
+<div class="container" id="infobox2" style="margin-top: 1ex; margin-bottom: 1ex;">
+<div class="journey" id="jdata"
+data-req="<%= stash('ajax_req') %>"
+data-route="<%= stash('ajax_route') %>"
+data-poly="<%= stash('ajax_polyline') %>"
+>
+ <strong><%= stash('train1_no') %></strong>
+ und
+ <strong><%= stash('train2_no') %></strong>
+ werden sich wahrscheinlich
+ % if (my $t = stash('time')) {
+ gegen <strong><%= $t->strftime('%H:%M') %> Uhr</strong>
+ % }
+ % if (my $p = stash('likely_pair')) {
+ zwischen <strong><%= $p->[0] %></strong> und <strong><%= $p->[1] %></strong>
+ % }
+ % if (not stash('time')) {
+ nicht
+ % }
+ begegnen.
+</div>
+</div>
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'
+% }
<div class="container">
<div id="map" style="height: 500px;">