summaryrefslogtreecommitdiff
path: root/lib/DBInfoscreen/Controller/Map.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/DBInfoscreen/Controller/Map.pm')
-rw-r--r--lib/DBInfoscreen/Controller/Map.pm475
1 files changed, 328 insertions, 147 deletions
diff --git a/lib/DBInfoscreen/Controller/Map.pm b/lib/DBInfoscreen/Controller/Map.pm
index d618e8c..7013b63 100644
--- a/lib/DBInfoscreen/Controller/Map.pm
+++ b/lib/DBInfoscreen/Controller/Map.pm
@@ -10,8 +10,17 @@ use Geo::Distance;
my $dbf_version = qx{git describe --dirty} || 'experimental';
+my $strp = DateTime::Format::Strptime->new(
+ pattern => '%Y-%m-%dT%H:%M:%S.000%z',
+ time_zone => 'Europe/Berlin',
+);
+
chomp $dbf_version;
+# Input: (HAFAS TripID, line number)
+# Output: Promise returning a
+# https://github.com/public-transport/hafas-client/blob/4/docs/trip.md instance
+# on success
sub get_hafas_polyline_p {
my ( $self, $trip_id, $line ) = @_;
@@ -63,7 +72,19 @@ sub get_hafas_polyline_p {
return $promise;
}
-sub estimate_train_position {
+# Input:
+# now: DateTime
+# from: current/previous stop
+# {dep => DateTime, name => str, lat => float, lon => float}
+# to: next stop
+# {arr => DateTime, name => str, lat => float, lon => float}
+# features: https://github.com/public-transport/hafas-client/blob/4/docs/trip.md features array
+# Output: list of estimated train positions in [lat, lon] format.
+# - current position
+# - position 2 seconds from now
+# - position 4 seconds from now
+# - ...
+sub estimate_train_positions {
my (%opt) = @_;
my $now = $opt{now};
@@ -74,8 +95,13 @@ sub estimate_train_position {
my $to_name = $opt{to}{name};
my $features = $opt{features};
- my $route_part_completion_ratio
- = ( $now->epoch - $from_dt->epoch ) / ( $to_dt->epoch - $from_dt->epoch );
+ my @train_positions;
+
+ my $time_complete = $now->epoch - $from_dt->epoch;
+ my $time_total = $to_dt->epoch - $from_dt->epoch;
+
+ my @completion_ratios
+ = map { ( $time_complete + ( $_ * 2 ) ) / $time_total } ( 0 .. 45 );
my $geo = Geo::Distance->new;
my ( $from_index, $to_index );
@@ -109,32 +135,214 @@ sub estimate_train_position {
);
}
}
- my $marker_distance = $total_distance * $route_part_completion_ratio;
+ 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]
);
- }
- if ( $total_distance > $marker_distance ) {
+ 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 );
+ }
- # return (lat, lon)
- return ( $this->[1], $this->[0] );
+ my $lat = $prev->[1]
+ + ( $this->[1] - $prev->[1] ) * $sub_ratio;
+ my $lon = $prev->[0]
+ + ( $this->[0] - $prev->[0] ) * $sub_ratio;
+
+ push( @train_positions, [ $lat, $lon ] );
+ }
+ }
+ if ( @train_positions == @completion_ratios ) {
+ return @train_positions;
+ }
}
}
+ if (@train_positions) {
+ return @train_positions;
+ }
}
else {
- my $lat = $opt{from}{lat}
- + ( $opt{to}{lat} - $opt{from}{lat} ) * $route_part_completion_ratio;
- my $lon = $opt{from}{lon}
- + ( $opt{to}{lon} - $opt{from}{lon} ) * $route_part_completion_ratio;
- return ( $lat, $lon );
+ for my $ratio (@completion_ratios) {
+ my $lat
+ = $opt{from}{lat} + ( $opt{to}{lat} - $opt{from}{lat} ) * $ratio;
+ my $lon
+ = $opt{from}{lon} + ( $opt{to}{lon} - $opt{from}{lon} ) * $ratio;
+ push( @train_positions, [ $lat, $lon ] );
+ }
+ return @train_positions;
+ }
+ return [ $opt{to}{lat}, $opt{to}{lon} ];
+}
+
+# Input:
+# now: DateTime
+# route: hash
+# lat: float
+# lon: float
+# name: str
+# arr: DateTime
+# dep: DateTime
+# features: ref to transport.rest features list
+# Output:
+# next_stop: {type, station}
+# positions: [current position [lat, lon], 2s from now, 4s from now, ...]
+sub estimate_train_positions2 {
+ my (%opt) = @_;
+ my $now = $opt{now};
+ my @route = @{ $opt{route} // [] };
+
+ my @train_positions;
+ my $next_stop;
+
+ for my $i ( 1 .. $#route ) {
+ if ( $route[$i]{arr}
+ and $route[ $i - 1 ]{dep}
+ and $now > $route[ $i - 1 ]{dep}
+ and $now < $route[$i]{arr} )
+ {
+
+ # (current position, future positons...) in 2 second steps
+ @train_positions = estimate_train_positions(
+ from => $route[ $i - 1 ],
+ to => $route[$i],
+ now => $now,
+ features => $opt{features},
+ );
+
+ $next_stop = {
+ type => 'next',
+ station => $route[$i],
+ };
+ last;
+ }
+ if ( $route[ $i - 1 ]{dep} and $now <= $route[ $i - 1 ]{dep} ) {
+ @train_positions
+ = ( [ $route[ $i - 1 ]{lat}, $route[ $i - 1 ]{lon} ] );
+ $next_stop = {
+ type => 'present',
+ station => $route[ $i - 1 ],
+ };
+ last;
+ }
}
- return ( $opt{to}{lat}, $opt{to}{lon} );
+
+ if ( not $next_stop ) {
+ @train_positions = ( [ $route[-1]{lat}, $route[-1]{lon} ] );
+ $next_stop = {
+ type => 'present',
+ station => $route[-1]
+ };
+ }
+
+ my $position_now = shift @train_positions;
+
+ return {
+ next_stop => $next_stop,
+ position_now => $position_now,
+ positions => \@train_positions,
+ };
+}
+
+sub route_to_ajax {
+ my (@stopovers) = @_;
+
+ my @route_entries;
+
+ for my $stop (@stopovers) {
+ my @stop_entries = ( $stop->{stop}{name} );
+ my $platform;
+
+ if ( $stop->{arrival}
+ and my $arr = $strp->parse_datetime( $stop->{arrival} ) )
+ {
+ my $delay = ( $stop->{arrivalDelay} // 0 ) / 60;
+ $platform = $stop->{arrivalPlatform};
+
+ push( @stop_entries, $arr->epoch, $delay );
+ }
+ else {
+ push( @stop_entries, q{}, q{} );
+ }
+
+ if ( $stop->{departure}
+ and my $dep = $strp->parse_datetime( $stop->{departure} ) )
+ {
+ my $delay = ( $stop->{departureDelay} // 0 ) / 60;
+ $platform //= $stop->{departurePlatform} // q{};
+
+ push( @stop_entries, $dep->epoch, $delay, $platform );
+ }
+ else {
+ push( @stop_entries, q{}, q{}, q{} );
+ }
+
+ push( @route_entries, join( ';', @stop_entries ) );
+ }
+
+ return join( '|', @route_entries );
+}
+
+# Input: List of transport.rest stopovers
+# Output: List of preprocessed stops. Each is a hash with the following keys:
+# lat: float
+# lon: float
+# name: str
+# arr: DateTime
+# dep: DateTime
+# arr_delay: int
+# dep_delay: int
+# platform: str
+sub stopovers_to_route {
+ my (@stopovers) = @_;
+ my @route;
+
+ for my $stop (@stopovers) {
+ my @stop_lines = ( $stop->{stop}{name} );
+ my ( $platform, $arr, $dep, $arr_delay, $dep_delay );
+
+ if ( $stop->{arrival}
+ and $arr = $strp->parse_datetime( $stop->{arrival} ) )
+ {
+ $arr_delay = ( $stop->{arrivalDelay} // 0 ) / 60;
+ $platform //= $stop->{arrivalPlatform};
+ }
+
+ if ( $stop->{departure}
+ and $dep = $strp->parse_datetime( $stop->{departure} ) )
+ {
+ $dep_delay = ( $stop->{departureDelay} // 0 ) / 60;
+ $platform //= $stop->{departurePlatform};
+ }
+
+ push(
+ @route,
+ {
+ lat => $stop->{stop}{location}{latitude},
+ lon => $stop->{stop}{location}{longitude},
+ name => $stop->{stop}{name},
+ arr => $arr,
+ dep => $dep,
+ arr_delay => $arr_delay,
+ dep_delay => $dep_delay,
+ platform => $platform,
+ }
+ );
+
+ }
+ return @route;
}
sub route {
@@ -154,17 +362,13 @@ sub route {
my @polyline = @{ $pl->{polyline} };
my @line_pairs;
my @station_coordinates;
- my @route;
my @markers;
my $next_stop;
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $strp = DateTime::Format::Strptime->new(
- pattern => '%Y-%m-%dT%H:%M:%S.000%z',
- time_zone => 'Europe/Berlin',
- );
+ 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,
@@ -175,164 +379,85 @@ sub route {
);
}
- for my $stop ( @{ $pl->{raw}{stopovers} // [] } ) {
- my @stop_lines = ( $stop->{stop}{name} );
- my ( $platform, $arr, $dep, $arr_delay, $dep_delay );
+ my @route = stopovers_to_route( @{ $pl->{raw}{stopovers} // [] } );
+
+ # Prepare from/to markers and name/time/delay overlays for stations
+ for my $stop (@route) {
+ my @stop_lines = ( $stop->{name} );
- if ( $from_name and $stop->{stop}{name} eq $from_name ) {
+ if ( $from_name and $stop->{name} eq $from_name ) {
push(
@markers,
{
- lon => $stop->{stop}{location}{longitude},
- lat => $stop->{stop}{location}{latitude},
- title => $stop->{stop}{name},
+ lon => $stop->{lon},
+ lat => $stop->{lat},
+ title => $stop->{name},
icon => 'goldIcon',
}
);
}
- if ( $to_name and $stop->{stop}{name} eq $to_name ) {
+ if ( $to_name and $stop->{name} eq $to_name ) {
push(
@markers,
{
- lon => $stop->{stop}{location}{longitude},
- lat => $stop->{stop}{location}{latitude},
- title => $stop->{stop}{name},
+ lon => $stop->{lon},
+ lat => $stop->{lat},
+ title => $stop->{name},
icon => 'greenIcon',
}
);
}
- if ( $stop->{arrival}
- and $arr = $strp->parse_datetime( $stop->{arrival} ) )
- {
- $arr_delay = ( $stop->{arrivalDelay} // 0 ) / 60;
- $platform //= $stop->{arrivalPlatform};
- my $arr_line = $arr->strftime('Ankunft: %H:%M');
- if ($arr_delay) {
- $arr_line .= sprintf( ' (%+d)', $arr_delay );
+ if ( $stop->{platform} ) {
+ push( @stop_lines, 'Gleis ' . $stop->{platform} );
+ }
+ if ( $stop->{arr} ) {
+ my $arr_line = $stop->{arr}->strftime('Ankunft: %H:%M');
+ if ( $stop->{arr_delay} ) {
+ $arr_line .= sprintf( ' (%+d)', $stop->{arr_delay} );
}
push( @stop_lines, $arr_line );
}
-
- if ( $stop->{departure}
- and $dep = $strp->parse_datetime( $stop->{departure} ) )
- {
- $dep_delay = ( $stop->{departureDelay} // 0 ) / 60;
- $platform //= $stop->{departurePlatform};
- my $dep_line = $dep->strftime('Abfahrt: %H:%M');
- if ($dep_delay) {
- $dep_line .= sprintf( ' (%+d)', $dep_delay );
+ if ( $stop->{dep} ) {
+ my $dep_line = $stop->{dep}->strftime('Abfahrt: %H:%M');
+ if ( $stop->{dep_delay} ) {
+ $dep_line .= sprintf( ' (%+d)', $stop->{dep_delay} );
}
push( @stop_lines, $dep_line );
}
- if ($platform) {
- splice( @stop_lines, 1, 0, "Gleis $platform" );
- }
-
- push(
- @station_coordinates,
- [
- [
- $stop->{stop}{location}{latitude},
- $stop->{stop}{location}{longitude}
- ],
- [@stop_lines],
- ]
- );
- push(
- @route,
- {
- lat => $stop->{stop}{location}{latitude},
- lon => $stop->{stop}{location}{longitude},
- name => $stop->{stop}{name},
- arr => $arr,
- dep => $dep,
- arr_delay => $arr_delay,
- dep_delay => $dep_delay,
- platform => $platform,
- }
- );
-
+ push( @station_coordinates,
+ [ [ $stop->{lat}, $stop->{lon} ], [@stop_lines], ] );
}
- for my $i ( 1 .. $#route ) {
- if ( $route[$i]{arr}
- and $route[ $i - 1 ]{dep}
- and $now > $route[ $i - 1 ]{dep}
- and $now < $route[$i]{arr} )
- {
-
- my $title = $pl->{name};
- if ( $route[$i]{arr_delay} ) {
- $title .= sprintf( ' (%+d)', $route[$i]{arr_delay} );
- }
-
- my ( $train_lat, $train_lon ) = estimate_train_position(
- from => $route[ $i - 1 ],
- to => $route[$i],
- now => $now,
- features => $pl->{raw}{polyline}{features},
- );
-
- push(
- @markers,
- {
- lat => $train_lat,
- lon => $train_lon,
- title => $title
- }
- );
+ my $train_pos = estimate_train_positions2(
+ now => $now,
+ route => \@route,
+ features => $pl->{raw}{polyline}{features},
+ );
- $next_stop = {
- type => 'next',
- station => $route[$i],
- };
- last;
- }
- if ( $route[ $i - 1 ]{dep} and $now <= $route[ $i - 1 ]{dep} ) {
- my $title = $pl->{name};
- if ( $route[$i]{arr_delay} ) {
- $title .= sprintf( ' (%+d)', $route[$i]{arr_delay} );
- }
- push(
- @markers,
- {
- lat => $route[ $i - 1 ]{lat},
- lon => $route[ $i - 1 ]{lon},
- title => $title
- }
- );
- $next_stop = {
- type => 'present',
- station => $route[ $i - 1 ],
- };
- last;
+ push(
+ @markers,
+ {
+ lat => $train_pos->{position_now}[0],
+ lon => $train_pos->{position_now}[1],
+ title => $pl->{name}
}
- }
- if ( not @markers ) {
- push(
- @markers,
- {
- lat => $route[-1]{lat},
- lon => $route[-1]{lon},
- title => $route[-1]{name} . ' - Endstation',
- }
- );
- $next_stop = {
- type => 'present',
- station => $route[-1]
- };
- }
+ );
+ $next_stop = $train_pos->{next_stop};
$self->render(
'route_map',
- title => $pl->{name},
- hide_opts => 1,
- with_map => 1,
- origin => {
+ title => $pl->{name},
+ hide_opts => 1,
+ with_map => 1,
+ ajax_req => "${trip_id}/${line_no}",
+ ajax_route => route_to_ajax( @{ $pl->{raw}{stopovers} // [] } ),
+ ajax_polyline => join( '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ),
+ origin => {
name => $pl->{raw}{origin}{name},
- ts => $pl->{raw}{dep_line}
+ ts => $pl->{raw}{departure}
? scalar $strp->parse_datetime( $pl->{raw}{departure} )
: undef,
},
@@ -374,4 +499,60 @@ sub route {
)->wait;
}
+sub ajax_route {
+ my ($self) = @_;
+ my $trip_id = $self->stash('tripid');
+ my $line_no = $self->stash('lineno');
+
+ delete $self->stash->{layout};
+
+ $self->render_later;
+
+ $self->get_hafas_polyline_p( $trip_id, $line_no )->then(
+ sub {
+ my ($pl) = @_;
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ my @route = stopovers_to_route( @{ $pl->{raw}{stopovers} // [] } );
+
+ my $train_pos = estimate_train_positions2(
+ now => $now,
+ route => \@route,
+ features => $pl->{raw}{polyline}{features},
+ );
+
+ my @polyline = @{ $pl->{polyline} };
+ $self->render(
+ '_map_infobox',
+ ajax_req => "${trip_id}/${line_no}",
+ ajax_route => route_to_ajax( @{ $pl->{raw}{stopovers} // [] } ),
+ ajax_polyline => join( '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ),
+ origin => {
+ name => $pl->{raw}{origin}{name},
+ ts => $pl->{raw}{departure}
+ ? scalar $strp->parse_datetime( $pl->{raw}{departure} )
+ : undef,
+ },
+ destination => {
+ name => $pl->{raw}{destination}{name},
+ ts => $pl->{raw}{arrival}
+ ? scalar $strp->parse_datetime( $pl->{raw}{arrival} )
+ : undef,
+ },
+ next_stop => $train_pos->{next_stop},
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ '_error',
+ error => $err,
+ );
+ }
+ )->wait;
+}
+
1;