diff options
author | Daniel Friesel <derf@finalrewind.org> | 2020-09-05 08:33:53 +0200 |
---|---|---|
committer | Daniel Friesel <derf@finalrewind.org> | 2020-09-05 08:33:53 +0200 |
commit | c8383c697ac9108b195696a5ca371e79d224225f (patch) | |
tree | 1e799fa158ba07ea264785f19635708855ac557c | |
parent | 8bf1dd3575bf58f1a50b28747f12f65a26b5a281 (diff) | |
parent | 37c510b2d351ea0c1c50ecceec21452de89afebf (diff) |
Merge branch 'master' of git.finalrewind.org:var/git_root/db-fakedisplay into master3.1.3
-rw-r--r-- | lib/DBInfoscreen.pm | 7 | ||||
-rw-r--r-- | lib/DBInfoscreen/Controller/Map.pm | 988 | ||||
-rw-r--r-- | lib/DBInfoscreen/Controller/Stationboard.pm | 624 | ||||
-rw-r--r-- | lib/DBInfoscreen/Controller/Wagenreihung.pm | 3 | ||||
-rw-r--r-- | public/static/js/map-refresh.js | 81 | ||||
-rw-r--r-- | public/static/js/map-refresh.min.js | 1 | ||||
-rw-r--r-- | templates/_error.html.ep | 5 | ||||
-rw-r--r-- | templates/_intersection_infobox.html.ep | 22 | ||||
-rw-r--r-- | templates/_map_infobox.html.ep | 61 | ||||
-rw-r--r-- | templates/layouts/app.html.ep | 1 | ||||
-rw-r--r-- | templates/route_map.html.ep | 64 |
11 files changed, 1262 insertions, 595 deletions
diff --git a/lib/DBInfoscreen.pm b/lib/DBInfoscreen.pm index 1828737..e7bdf80 100644 --- a/lib/DBInfoscreen.pm +++ b/lib/DBInfoscreen.pm @@ -142,8 +142,9 @@ sub startup { $self->helper( 'handle_no_results_json' => sub { - my ( $self, $backend, $station, $errstr, $api_version, $callback ) - = @_; + my ( $self, $backend, $station, $errstr, $api_version ) = @_; + + my $callback = $self->param('callback'); $self->res->headers->access_control_allow_origin(q{*}); my $json; @@ -315,7 +316,9 @@ sub startup { $r->get('/_wr/:train/:departure')->to('wagenreihung#wagenreihung'); + $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 8ff68d7..4497a4a 100644 --- a/lib/DBInfoscreen/Controller/Map.pm +++ b/lib/DBInfoscreen/Controller/Map.pm @@ -7,11 +7,21 @@ use Mojo::Promise; use DateTime; use DateTime::Format::Strptime; use Geo::Distance; +use List::Util qw(); 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 ) = @_; @@ -22,6 +32,7 @@ sub get_hafas_polyline_p { if ( my $content = $cache->thaw($url) ) { $promise->resolve($content); + $self->app->log->debug("GET $url (cached)"); return $promise; } @@ -31,6 +42,7 @@ sub get_hafas_polyline_p { ->then( sub { my ($tx) = @_; + $self->app->log->debug("GET $url (OK)"); my $json = decode_json( $tx->res->body ); my @coordinate_list; @@ -56,6 +68,7 @@ sub get_hafas_polyline_p { )->catch( sub { my ($err) = @_; + $self->app->log->debug("GET $url (error: $err)"); $promise->reject($err); } )->wait; @@ -63,6 +76,646 @@ 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 +# {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}; + + 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 @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 ) + = 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 ) { + 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, [ $lat, $lon ] ); + } + } + if ( @train_positions == @completion_ratios ) { + return @train_positions; + } + } + } + if (@train_positions) { + return @train_positions; + } + } + else { + 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; + } + } + + 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 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) = @_; + + 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 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'); @@ -78,262 +731,95 @@ sub route { my ($pl) = @_; 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' ); - 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); - 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} // [] } ); - if ( $from_name and $stop->{stop}{name} eq $from_name ) { + # 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->{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 $from_dt = $route[ $i - 1 ]{dep}; - my $to_dt = $route[$i]{arr}; - my $from_name = $route[ $i - 1 ]{name}; - my $to_name = $route[$i]{name}; - - my $route_part_completion_ratio - = ( $now->epoch - $from_dt->epoch ) - / ( $to_dt->epoch - $from_dt->epoch ); - - my $title = $pl->{name}; - if ( $route[$i]{arr_delay} ) { - $title .= sprintf( ' (%+d)', $route[$i]{arr_delay} ); - } - - my $geo = Geo::Distance->new; - my ( $from_index, $to_index ); + my $train_pos = estimate_train_positions2( + now => $now, + route => \@route, + features => $pl->{raw}{polyline}{features}, + ); - for my $j ( 0 .. $#{ $pl->{raw}{polyline}{features} } ) { - my $this_point = $pl->{raw}{polyline}{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; - } - } - if ( $from_index and $to_index ) { - my $total_distance = 0; - for my $j ( $from_index + 1 .. $to_index ) { - my $prev = $pl->{raw}{polyline}{features}[ $j - 1 ] - {geometry}{coordinates}; - my $this - = $pl->{raw}{polyline}{features}[$j]{geometry} - {coordinates}; - if ( $prev and $this ) { - $total_distance += $geo->distance( - 'kilometer', $prev->[0], $prev->[1], - $this->[0], $this->[1] - ); - } - } - my $marker_distance - = $total_distance * $route_part_completion_ratio; - $total_distance = 0; - for my $j ( $from_index + 1 .. $to_index ) { - my $prev = $pl->{raw}{polyline}{features}[ $j - 1 ] - {geometry}{coordinates}; - my $this - = $pl->{raw}{polyline}{features}[$j]{geometry} - {coordinates}; - if ( $prev and $this ) { - $total_distance += $geo->distance( - 'kilometer', $prev->[0], $prev->[1], - $this->[0], $this->[1] - ); - } - if ( $total_distance > $marker_distance ) { - push( - @markers, - { - lon => $this->[0], - lat => $this->[1], - title => $title - } - ); - $next_stop = { - type => 'next', - station => $route[$i], - }; - last; - } - } - } - else { - push( - @markers, - { - lat => $route[ $i - 1 ]{lat} + ( - ( $route[$i]{lat} - $route[ $i - 1 ]{lat} ) - * $route_part_completion_ratio - ), - lon => $route[ $i - 1 ]{lon} + ( - ( $route[$i]{lon} - $route[ $i - 1 ]{lon} ) - * $route_part_completion_ratio - ), - title => $title - } - ); - $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, }, @@ -363,14 +849,68 @@ 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, + ); + + } + )->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; } diff --git a/lib/DBInfoscreen/Controller/Stationboard.pm b/lib/DBInfoscreen/Controller/Stationboard.pm index 4a9c94d..253cbcb 100644 --- a/lib/DBInfoscreen/Controller/Stationboard.pm +++ b/lib/DBInfoscreen/Controller/Stationboard.pm @@ -11,6 +11,7 @@ use File::Slurp qw(read_file write_file); use List::Util qw(max); use List::MoreUtils qw(); use Mojo::JSON qw(decode_json); +use Mojo::Promise; use Travel::Status::DE::HAFAS; use Travel::Status::DE::IRIS; use Travel::Status::DE::IRIS::Stations; @@ -504,25 +505,13 @@ sub get_results_for { } sub handle_request { - my ($self) = @_; + my ($self) = @_; my $station = $self->stash('station'); - my $via = $self->param('via'); - my @platforms = split( /,/, $self->param('platforms') // q{} ); - my @lines = split( /,/, $self->param('lines') // q{} ); - my $template = $self->param('mode') // 'app'; - my $hide_low_delay = $self->param('hidelowdelay') // 0; - my $hide_opts = $self->param('hide_opts') // 0; - my $show_realtime = $self->param('show_realtime') // 0; - my $show_details = $self->param('detailed') // 0; - my $backend = $self->param('backend') // 'iris'; - my $admode = $self->param('admode') // 'deparr'; - my $apiver = $self->param('version') // 0; - my $callback = $self->param('callback'); - my $with_related = !$self->param('no_related'); - my $limit = $self->param('limit') // 0; - my @train_types = split( /,/, $self->param('train_types') // q{} ); - my %opt = ( + my $template = $self->param('mode') // 'app'; + my $backend = $self->param('backend') // 'iris'; + my $with_related = !$self->param('no_related'); + my %opt = ( cache_hafas => $self->app->cache_hafas, cache_iris_main => $self->app->cache_iris_main, cache_iris_rt => $self->app->cache_iris_rt, @@ -568,6 +557,8 @@ sub handle_request { $template = 'app'; } + $self->param( mode => $template ); + if ( not $station ) { $self->render( 'landingpage', show_intro => 1 ); return; @@ -582,35 +573,312 @@ sub handle_request { $opt{with_related} = 1; } - my @departures; - my $data = get_results_for( $backend, $station, %opt ); - my $results_ref = $data->{results}; - my $errstr = $data->{errstr}; - my @results = @{$results_ref}; + my $data = get_results_for( $backend, $station, %opt ); + my $errstr = $data->{errstr}; - if ( not @results and $template eq 'json' ) { + if ( not @{ $data->{results} } and $template eq 'json' ) { $self->handle_no_results_json( $backend, $station, $errstr, - $api_version, $callback ); + $api_version ); return; } - # foo/bar used to mean "departures for foo via bar". This is now - # deprecated, but most of these cases are handled here. - if ( not @results and $station =~ m{/} ) { - ( $station, $via ) = split( qr{/}, $station ); - $self->param( station => $station ); - $self->param( via => $via ); - $data = get_results_for( $backend, $station, %opt ); - $results_ref = $data->{results}; - $errstr = $data->{errstr}; - @results = @{$results_ref}; - } - - if ( not @results ) { + if ( not @{ $data->{results} } ) { $self->handle_no_results( $backend, $station, $errstr ); return; } + $self->handle_result($data); +} + +sub filter_results { + my ( $self, @results ) = @_; + + if ( my $train = $self->param('train') ) { + @results = grep { result_is_train( $_, $train ) } @results; + } + + if ( my @lines = split( /,/, $self->param('lines') // q{} ) ) { + @results = grep { result_has_line( $_, @lines ) } @results; + } + + if ( my @platforms = split( /,/, $self->param('platforms') // q{} ) ) { + @results = grep { result_has_platform( $_, @platforms ) } @results; + } + + if ( my $via = $self->param('via') ) { + $via =~ s{ , \s* }{|}gx; + @results = grep { result_has_via( $_, $via ) } @results; + } + + if ( my @train_types = split( /,/, $self->param('train_types') // q{} ) ) { + @results = grep { result_has_train_type( $_, @train_types ) } @results; + } + + if ( my $limit = $self->param('limit') ) { + if ( $limit =~ m{ ^ \d+ $ }x ) { + splice( @results, $limit ); + } + } + + return @results; +} + +sub format_iris_result_info { + my ( $self, $template, $result ) = @_; + my ( $info, $moreinfo ); + + my $delaymsg + = join( ', ', map { $_->[1] } $result->delay_messages ); + my $qosmsg = join( ' +++ ', map { $_->[1] } $result->qos_messages ); + if ( $result->is_cancelled ) { + $info = "Fahrt fällt aus: ${delaymsg}"; + } + elsif ( $result->departure_is_cancelled ) { + $info = "Zug endet hier: ${delaymsg}"; + } + elsif ( $result->delay and $result->delay > 0 ) { + if ( $template eq 'app' or $template eq 'infoscreen' ) { + $info = $delaymsg; + } + else { + $info = sprintf( 'ca. +%d%s%s', + $result->delay, $delaymsg ? q{: } : q{}, $delaymsg ); + } + } + if ( $result->replacement_for + and $template ne 'app' + and $template ne 'infoscreen' ) + { + for my $rep ( $result->replacement_for ) { + $info = sprintf( + 'Ersatzzug für %s %s %s%s', + $rep->type, $rep->train_no, + $info ? '+++ ' : q{}, $info // q{} + ); + } + } + if ( $info and $qosmsg ) { + $info .= ' +++ '; + } + $info .= $qosmsg; + + if ( $result->additional_stops and not $result->is_cancelled ) { + my $additional_line = join( q{, }, $result->additional_stops ); + $info + = 'Zusätzliche Halte: ' + . $additional_line + . ( $info ? ' +++ ' : q{} ) + . $info; + if ( $template ne 'json' ) { + push( @{$moreinfo}, [ 'Zusätzliche Halte', $additional_line ] ); + } + } + + if ( $result->canceled_stops and not $result->is_cancelled ) { + my $cancel_line = join( q{, }, $result->canceled_stops ); + $info + = 'Ohne Halt in: ' . $cancel_line . ( $info ? ' +++ ' : q{} ) . $info; + if ( $template ne 'json' ) { + push( @{$moreinfo}, [ 'Ohne Halt in', $cancel_line ] ); + } + } + + push( @{$moreinfo}, $result->messages ); + + return ( $info, $moreinfo ); +} + +sub format_hafas_result_info { + my ( $self, $result ) = @_; + my ( $info, $moreinfo ); + + $info = $result->info; + if ($info) { + $moreinfo = [ [ 'HAFAS', $info ] ]; + } + if ( $result->delay and $result->delay > 0 ) { + if ($info) { + $info = 'ca. +' . $result->delay . ': ' . $info; + } + else { + $info = 'ca. +' . $result->delay; + } + } + push( @{$moreinfo}, map { [ 'HAFAS', $_ ] } $result->messages ); + + return ( $info, $moreinfo ); +} + +sub render_train { + my ( $self, $result, $departure, $station_name ) = @_; + + $departure->{route_pre_diff} = [ + $self->json_route_diff( + [ $result->route_pre ], + [ $result->sched_route_pre ] + ) + ]; + $departure->{route_post_diff} = [ + $self->json_route_diff( + [ $result->route_post ], + [ $result->sched_route_post ] + ) + ]; + + $departure->{trip_id} + = get_hafas_trip_id( $self->ua, $self->app->cache_iris_main, $result ); + + if ( + $departure->{wr_link} + and not check_wagonorder_with_wings( + $self->ua, $self->app->cache_iris_main, + $result, $departure->{wr_link} + ) + ) + { + $departure->{wr_link} = undef; + } + + my ( $route_ts, $route_info ) = get_route_timestamps( + $self->ua, + $self->app->cache_iris_main, + $self->app->cache_iris_rt, + { train => $result } + ); + + # If a train number changes on the way, IRIS routes are incomplete, + # whereas HAFAS data has all stops -> merge HAFAS stops into IRIS + # stops. This is a rare case, one point where it can be observed is + # the TGV service at Frankfurt/Karlsruhe/Mannheim. + if ( $route_info + and my @hafas_stations = @{ $route_info->{stations} // [] } ) + { + if ( my @iris_stations = @{ $departure->{route_pre_diff} } ) { + my @missing_pre; + for my $station (@hafas_stations) { + if ( + List::MoreUtils::any { $_->{name} eq $station } + @iris_stations + ) + { + unshift( @{ $departure->{route_pre_diff} }, @missing_pre ); + last; + } + push( + @missing_pre, + { + name => $station, + hafas => 1 + } + ); + } + } + if ( my @iris_stations = @{ $departure->{route_post_diff} } ) { + my @missing_post; + for my $station ( reverse @hafas_stations ) { + if ( + List::MoreUtils::any { $_->{name} eq $station } + @iris_stations + ) + { + push( @{ $departure->{route_post_diff} }, @missing_post ); + last; + } + unshift( + @missing_post, + { + name => $station, + hafas => 1 + } + ); + } + } + } + if ($route_ts) { + for my $elem ( + @{ $departure->{route_pre_diff} }, + @{ $departure->{route_post_diff} } + ) + { + for my $key ( keys %{ $route_ts->{ $elem->{name} } // {} } ) { + $elem->{$key} = $route_ts->{ $elem->{name} }{$key}; + } + } + } + if ( $route_info and @{ $route_info->{messages} // [] } ) { + my $him = $route_info->{messages}; + my @him_messages; + $departure->{messages}{him} = $him; + for my $message ( @{$him} ) { + if ( $message->{display} ) { + push( @him_messages, [ $message->{header}, $message->{lead} ] ); + } + } + for my $message ( @{ $departure->{moreinfo} // [] } ) { + my $m = $message->[1]; + @him_messages + = grep { $_->[0] !~ m{Information\. $m\.$} } @him_messages; + } + unshift( @{ $departure->{moreinfo} }, @him_messages ); + } + + my $linetype = 'bahn'; + if ( $departure->{train_type} eq 'S' ) { + $linetype = 'sbahn'; + } + elsif ($departure->{train_type} eq 'IC' + or $departure->{train_type} eq 'ICE' + or $departure->{train_type} eq 'EC' + or $departure->{train_type} eq 'ECE' + or $departure->{train_type} eq 'EN' ) + { + $linetype = 'fern'; + } + elsif ($departure->{train_type} eq 'THA' + or $departure->{train_type} eq 'TGV' + or $departure->{train_type} eq 'FLX' + or $departure->{train_type} eq 'NJ' ) + { + $linetype = 'ext'; + } + elsif ( $departure->{train_line} + and $departure->{train_line} =~ m{^S\d} ) + { + $linetype = 'sbahn'; + } + + $self->render( + '_train_details', + departure => $departure, + linetype => $linetype, + icetype => $self->app->ice_type_map->{ $departure->{train_no} }, + dt_now => DateTime->now( time_zone => 'Europe/Berlin' ), + station_name => $station_name, + ); +} + +sub handle_result { + my ( $self, $data ) = @_; + + my @results = @{ $data->{results} }; + my @departures; + + my @platforms = split( /,/, $self->param('platforms') // q{} ); + my $template = $self->param('mode') // 'app'; + my $hide_low_delay = $self->param('hidelowdelay') // 0; + my $hide_opts = $self->param('hide_opts') // 0; + my $show_realtime = $self->param('show_realtime') // 0; + my $show_details = $self->param('detailed') // 0; + my $backend = $self->param('backend') // 'iris'; + my $admode = $self->param('admode') // 'deparr'; + my $apiver = $self->param('version') // 0; + my $callback = $self->param('callback'); + my $via = $self->param('via'); + + if ( $self->param('ajax') ) { + delete $self->stash->{layout}; + } + if ( $template eq 'single' ) { if ( not @platforms ) { for my $result (@results) { @@ -649,30 +917,7 @@ sub handle_request { } } - if ( my $train = $self->param('train') ) { - @results = grep { result_is_train( $_, $train ) } @results; - } - - if (@lines) { - @results = grep { result_has_line( $_, @lines ) } @results; - } - - if (@platforms) { - @results = grep { result_has_platform( $_, @platforms ) } @results; - } - - if ($via) { - $via =~ s{ , \s* }{|}gx; - @results = grep { result_has_via( $_, $via ) } @results; - } - - if (@train_types) { - @results = grep { result_has_train_type( $_, @train_types ) } @results; - } - - if ( $limit and $limit =~ m{ ^ \d+ $ }x ) { - splice( @results, $limit ); - } + @results = $self->filter_results(@results); for my $result (@results) { my $platform = ( split( qr{ }, $result->platform // '' ) )[0]; @@ -689,84 +934,11 @@ sub handle_request { } my ( $info, $moreinfo ); if ( $backend eq 'iris' ) { - my $delaymsg - = join( ', ', map { $_->[1] } $result->delay_messages ); - my $qosmsg = join( ' +++ ', map { $_->[1] } $result->qos_messages ); - if ( $result->is_cancelled ) { - $info = "Fahrt fällt aus: ${delaymsg}"; - } - elsif ( $result->departure_is_cancelled ) { - $info = "Zug endet hier: ${delaymsg}"; - } - elsif ( $result->delay and $result->delay > 0 ) { - if ( $template eq 'app' or $template eq 'infoscreen' ) { - $info = $delaymsg; - } - else { - $info = sprintf( 'ca. +%d%s%s', - $result->delay, $delaymsg ? q{: } : q{}, $delaymsg ); - } - } - if ( $result->replacement_for - and $template ne 'app' - and $template ne 'infoscreen' ) - { - for my $rep ( $result->replacement_for ) { - $info = sprintf( - 'Ersatzzug für %s %s %s%s', - $rep->type, $rep->train_no, - $info ? '+++ ' : q{}, $info // q{} - ); - } - } - if ( $info and $qosmsg ) { - $info .= ' +++ '; - } - $info .= $qosmsg; - - if ( $result->additional_stops and not $result->is_cancelled ) { - my $additional_line = join( q{, }, $result->additional_stops ); - $info - = 'Zusätzliche Halte: ' - . $additional_line - . ( $info ? ' +++ ' : q{} ) - . $info; - if ( $template ne 'json' ) { - push( - @{$moreinfo}, - [ 'Zusätzliche Halte', $additional_line ] - ); - } - } - - if ( $result->canceled_stops and not $result->is_cancelled ) { - my $cancel_line = join( q{, }, $result->canceled_stops ); - $info - = 'Ohne Halt in: ' - . $cancel_line - . ( $info ? ' +++ ' : q{} ) - . $info; - if ( $template ne 'json' ) { - push( @{$moreinfo}, [ 'Ohne Halt in', $cancel_line ] ); - } - } - - push( @{$moreinfo}, $result->messages ); + ( $info, $moreinfo ) + = $self->format_iris_result_info( $template, $result ); } else { - $info = $result->info; - if ($info) { - $moreinfo = [ [ 'HAFAS', $info ] ]; - } - if ( $result->delay and $result->delay > 0 ) { - if ($info) { - $info = 'ca. +' . $result->delay . ': ' . $info; - } - else { - $info = 'ca. +' . $result->delay; - } - } - push( @{$moreinfo}, map { [ 'HAFAS', $_ ] } $result->messages ); + ( $info, $moreinfo ) = $self->format_hafas_result_info($result); } my $time = $result->time; @@ -1026,131 +1198,9 @@ sub handle_request { } ); if ( $self->param('train') ) { - $departures[-1]{route_pre_diff} = [ - $self->json_route_diff( - [ $result->route_pre ], - [ $result->sched_route_pre ] - ) - ]; - $departures[-1]{route_post_diff} = [ - $self->json_route_diff( - [ $result->route_post ], - [ $result->sched_route_post ] - ) - ]; - - $departures[-1]{trip_id} - = get_hafas_trip_id( $self->ua, $self->app->cache_iris_main, - $result ); - - if ( - $departures[-1]{wr_link} - and not check_wagonorder_with_wings( - $self->ua, $self->app->cache_iris_main, - $result, $departures[-1]{wr_link} - ) - ) - { - $departures[-1]{wr_link} = undef; - } - - my ( $route_ts, $route_info ) = get_route_timestamps( - $self->ua, - $self->app->cache_iris_main, - $self->app->cache_iris_rt, - { train => $result } - ); - - # If a train number changes on the way, IRIS routes are incomplete, - # whereas HAFAS data has all stops -> merge HAFAS stops into IRIS - # stops. This is a rare case, one point where it can be observed is - # the TGV service at Frankfurt/Karlsruhe/Mannheim. - if ( $route_info - and my @hafas_stations - = @{ $route_info->{stations} // [] } ) - { - if ( my @iris_stations - = @{ $departures[-1]{route_pre_diff} } ) - { - my @missing_pre; - for my $station (@hafas_stations) { - if ( - List::MoreUtils::any { $_->{name} eq $station } - @iris_stations - ) - { - unshift( - @{ $departures[-1]{route_pre_diff} }, - @missing_pre - ); - last; - } - push( - @missing_pre, - { - name => $station, - hafas => 1 - } - ); - } - } - if ( my @iris_stations - = @{ $departures[-1]{route_post_diff} } ) - { - my @missing_post; - for my $station ( reverse @hafas_stations ) { - if ( - List::MoreUtils::any { $_->{name} eq $station } - @iris_stations - ) - { - push( - @{ $departures[-1]{route_post_diff} }, - @missing_post - ); - last; - } - unshift( - @missing_post, - { - name => $station, - hafas => 1 - } - ); - } - } - } - if ($route_ts) { - for my $elem ( - @{ $departures[-1]{route_pre_diff} }, - @{ $departures[-1]{route_post_diff} } - ) - { - for my $key ( - keys %{ $route_ts->{ $elem->{name} } // {} } ) - { - $elem->{$key} = $route_ts->{ $elem->{name} }{$key}; - } - } - } - if ( $route_info and @{ $route_info->{messages} // [] } ) { - my $him = $route_info->{messages}; - my @him_messages; - $departures[-1]{messages}{him} = $him; - for my $message ( @{$him} ) { - if ( $message->{display} ) { - push( @him_messages, - [ $message->{header}, $message->{lead} ] ); - } - } - for my $message ( @{ $departures[-1]{moreinfo} // [] } ) { - my $m = $message->[1]; - @him_messages - = grep { $_->[0] !~ m{Information\. $m\.$} } - @him_messages; - } - unshift( @{ $departures[-1]{moreinfo} }, @him_messages ); - } + $self->render_train( $result, $departures[-1], + $data->{station_name} // $self->stash('station') ); + return; } } else { @@ -1182,10 +1232,6 @@ sub handle_request { } } - if ( $self->param('ajax') ) { - delete $self->stash->{layout}; - } - if ( $template eq 'json' ) { $self->res->headers->access_control_allow_origin(q{*}); my $json = $self->render_to_string( @@ -1223,52 +1269,8 @@ sub handle_request { format => 'text', ); } - elsif ( my $train = $self->param('train') ) { - - my ($departure) = @departures; - - if ($departure) { - - my $linetype = 'bahn'; - if ( $departure->{train_type} eq 'S' ) { - $linetype = 'sbahn'; - } - elsif ($departure->{train_type} eq 'IC' - or $departure->{train_type} eq 'ICE' - or $departure->{train_type} eq 'EC' - or $departure->{train_type} eq 'ECE' - or $departure->{train_type} eq 'EN' ) - { - $linetype = 'fern'; - } - elsif ($departure->{train_type} eq 'THA' - or $departure->{train_type} eq 'TGV' - or $departure->{train_type} eq 'FLX' - or $departure->{train_type} eq 'NJ' ) - { - $linetype = 'ext'; - } - elsif ( $departure->{train_line} - and $departure->{train_line} =~ m{^S\d} ) - { - $linetype = 'sbahn'; - } - - $self->render( - '_train_details', - departure => $departure, - linetype => $linetype, - icetype => $self->app->ice_type_map->{ $departure->{train_no} }, - dt_now => DateTime->now( time_zone => 'Europe/Berlin' ), - station_name => $data->{station_name} // $station, - ); - } - else { - $self->render('not_found'); - } - } else { - my $station_name = $data->{station_name} // $station; + my $station_name = $data->{station_name} // $self->stash('station'); $self->render( $template, departures => \@departures, diff --git a/lib/DBInfoscreen/Controller/Wagenreihung.pm b/lib/DBInfoscreen/Controller/Wagenreihung.pm index a87bb29..7b59227 100644 --- a/lib/DBInfoscreen/Controller/Wagenreihung.pm +++ b/lib/DBInfoscreen/Controller/Wagenreihung.pm @@ -25,6 +25,7 @@ sub get_wagenreihung_p { if ( my $content = $cache->thaw($url) ) { $promise->resolve($content); + $self->app->log->debug("GET $url (cached)"); return $promise; } @@ -33,6 +34,7 @@ sub get_wagenreihung_p { ->then( sub { my ($tx) = @_; + $self->app->log->debug("GET $url (OK)"); my $body = decode( 'utf-8', $tx->res->body ); my $json = JSON->new->decode($body); @@ -42,6 +44,7 @@ sub get_wagenreihung_p { )->catch( sub { my ($err) = @_; + $self->app->log->debug("GET $url (error: $err)"); $promise->reject($err); } )->wait; diff --git a/public/static/js/map-refresh.js b/public/static/js/map-refresh.js new file mode 100644 index 0000000..b1729c3 --- /dev/null +++ b/public/static/js/map-refresh.js @@ -0,0 +1,81 @@ +var j_reqid; +//var j_stops = []; +var j_positions = []; +var j_frame = []; +var j_frame_i = []; + +function dbf_map_parse() { + $('#jdata').each(function() { + j_reqid = $(this).data('req'); + /*var route_data = $(this).data('route'); + if (route_data) { + route_data = route_data.split('|'); + j_stops = []; + for (var stop_id in route_data) { + var stopdata = route_data[stop_id].split(';'); + for (var i = 1; i < 5; i++) { + stopdata[i] = parseInt(stopdata[i]); + } + j_stops.push(stopdata); + } + }*/ + var positions = $(this).data('poly'); + if (positions) { + positions = positions.split('|'); + j_positions = []; + for (var pos_id in positions) { + var posdata = positions[pos_id].split(';'); + posdata[0] = parseFloat(posdata[0]); + posdata[1] = parseFloat(posdata[1]); + j_positions.push(posdata); + } + } + }); +} + +function dbf_anim_coarse() { + if (j_positions.length) { + var pos1 = marker.getLatLng(); + var pos1lat = pos1.lat; + var pos1lon = pos1.lng; + var pos2 = j_positions.shift(); + var pos2lat = pos2[0]; + var pos2lon = pos2[1]; + + j_frame_i = 200; + j_frame = []; + + // approx 30 Hz -> 60 frames per 2 seconds + for (var i = 1; i <= 60; i++) { + var ratio = i / 60; + j_frame.push([pos1lat + ((pos2lat - pos1lat) * ratio), pos1lon + ((pos2lon - pos1lon) * ratio)]); + } + + j_frame_i = 0; + } +} + +function dbf_anim_fine() { + if (j_frame[j_frame_i]) { + marker.setLatLng(j_frame[j_frame_i++]); + } +} + +function dbf_map_reload() { + $.get('/_ajax_mapinfo/' + j_reqid, function(data) { + $('#infobox').html(data); + dbf_map_parse(); + setTimeout(dbf_map_reload, 61000); + }).fail(function() { + setTimeout(dbf_map_reload, 5000); + }); +} + +$(document).ready(function() { + if ($('#infobox').length) { + dbf_map_parse(); + setInterval(dbf_anim_coarse, 2000); + setInterval(dbf_anim_fine, 33); + setTimeout(dbf_map_reload, 61000); + } +}); diff --git a/public/static/js/map-refresh.min.js b/public/static/js/map-refresh.min.js new file mode 100644 index 0000000..802d376 --- /dev/null +++ b/public/static/js/map-refresh.min.js @@ -0,0 +1 @@ +function dbf_map_parse(){$("#jdata").each(function(){j_reqid=$(this).data("req");var a=$(this).data("poly");if(a){a=a.split("|"),j_positions=[];for(var e in a){var i=a[e].split(";");i[0]=parseFloat(i[0]),i[1]=parseFloat(i[1]),j_positions.push(i)}}})}function dbf_anim_coarse(){if(j_positions.length){var a=marker.getLatLng(),e=a.lat,i=a.lng,_=j_positions.shift(),t=_[0],r=_[1];j_frame_i=200,j_frame=[];for(var f=1;f<=60;f++){var n=f/60;j_frame.push([e+(t-e)*n,i+(r-i)*n])}j_frame_i=0}}function dbf_anim_fine(){j_frame[j_frame_i]&&marker.setLatLng(j_frame[j_frame_i++])}function dbf_map_reload(){$.get("/_ajax_mapinfo/"+j_reqid,function(a){$("#infobox").html(a),dbf_map_parse(),setTimeout(dbf_map_reload,61e3)}).fail(function(){setTimeout(dbf_map_reload,5e3)})}var j_reqid,j_positions=[],j_frame=[],j_frame_i=[];$(document).ready(function(){$("#infobox").length&&(dbf_map_parse(),setInterval(dbf_anim_coarse,2e3),setInterval(dbf_anim_fine,33),setTimeout(dbf_map_reload,61e3))}); diff --git a/templates/_error.html.ep b/templates/_error.html.ep new file mode 100644 index 0000000..4abfb4e --- /dev/null +++ b/templates/_error.html.ep @@ -0,0 +1,5 @@ +<div class="error"><strong>Fehler:</strong> +<pre> +%= $error +</pre> +</div> 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/_map_infobox.html.ep b/templates/_map_infobox.html.ep new file mode 100644 index 0000000..8f66a6f --- /dev/null +++ b/templates/_map_infobox.html.ep @@ -0,0 +1,61 @@ +<div class="container" id="infobox" 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') %>" +> + Fahrt + % if (stash('train_no')) { + <strong><%= stash('train_no') %></strong> + % } + von <strong><%= stash('origin')->{name} %></strong> + nach <strong><%= stash('destination')->{name} %></strong> +</div> +% if (my $next = stash('next_stop')) { + <div class="nextstop"> + % if ($next->{type} eq 'present' and $next->{station}{dep} and $next->{station}{arr}) { + Aufenthalt in <strong><%= $next->{station}{name} %></strong> + an Gleis <strong><%= $next->{station}{platform} %></strong> + bis <strong><%= $next->{station}{dep}->strftime('%H:%M') %></strong> + % if ($next->{station}{dep_delay}) { + %= sprintf('(%+d)', $next->{station}{dep_delay}) + % } + % } + % elsif ($next->{type} eq 'present' and $next->{station}{dep}) { + Abfahrt in <strong><%= $next->{station}{name} %></strong> + von Gleis <strong><%= $next->{station}{platform} %></strong> + um <strong><%= $next->{station}{dep}->strftime('%H:%M') %></strong> + % if ($next->{station}{dep_delay}) { + %= sprintf('(%+d)', $next->{station}{dep_delay}) + % } + % } + % elsif ($next->{type} eq 'present' and $next->{station}{arr}) { + Endstation erreicht um + <strong><%= $next->{station}{arr}->strftime('%H:%M') %></strong> + auf Gleis <strong><%= $next->{station}{platform} %></strong> + % if ($next->{station}{arr_delay}) { + %= sprintf('(%+d)', $next->{station}{arr_delay}) + % } + % } + % elsif ($next->{type} eq 'present') { + Zug steht in + <strong><%= $next->{station}{arr}->strftime('%H:%M') %></strong> + auf Gleis <strong><%= $next->{station}{platform} %></strong> + % } + % elsif ($next->{type} eq 'next' and $next->{station}{arr}) { + Nächster Halt: + <strong><%= $next->{station}{name} %></strong> + um <strong><%= $next->{station}{arr}->strftime('%H:%M') %></strong> + % if ($next->{station}{arr_delay}) { + %= sprintf('(%+d)', $next->{station}{arr_delay}) + % } + auf Gleis <strong><%= $next->{station}{platform} %></strong> + % } + % elsif ($next->{type} eq 'next') { + Nächster Halt: + <strong><%= $next->{station}{name} %></strong> + auf Gleis <strong><%= $next->{station}{platform} %></strong> + % } + </div> +% } +</div> diff --git a/templates/layouts/app.html.ep b/templates/layouts/app.html.ep index d7320de..fc81ba3 100644 --- a/templates/layouts/app.html.ep +++ b/templates/layouts/app.html.ep @@ -61,6 +61,7 @@ % if (stash('with_map')) { %= stylesheet "/static/${av}/leaflet/leaflet.css" %= javascript "/static/${av}/leaflet/leaflet.js" + %= javascript "/static/${av}/js/map-refresh.min.js", defer => undef % } </head> <body> diff --git a/templates/route_map.html.ep b/templates/route_map.html.ep index 6ed1b57..1ba96bc 100644 --- a/templates/route_map.html.ep +++ b/templates/route_map.html.ep @@ -1,58 +1,8 @@ -% if ($origin and $destination) { - <div class="container" style="margin-top: 1ex; margin-bottom: 1ex;"> - Fahrt - % if (stash('train_no')) { - <strong><%= stash('train_no') %></strong> - % } - von <strong><%= $origin->{name} %></strong> - nach <strong><%= $destination->{name} %></strong> - % if (my $next = stash('next_stop')) { - <br/> - % if ($next->{type} eq 'present' and $next->{station}{dep} and $next->{station}{arr}) { - Aufenthalt in <strong><%= $next->{station}{name} %></strong> - an Gleis <strong><%= $next->{station}{platform} %></strong> - bis <strong><%= $next->{station}{dep}->strftime('%H:%M') %></strong> - % if ($next->{station}{dep_delay}) { - %= sprintf('(%+d)', $next->{station}{dep_delay}) - % } - % } - % elsif ($next->{type} eq 'present' and $next->{station}{dep}) { - Abfahrt in <strong><%= $next->{station}{name} %></strong> - von Gleis <strong><%= $next->{station}{platform} %></strong> - um <strong><%= $next->{station}{dep}->strftime('%H:%M') %></strong> - % if ($next->{station}{dep_delay}) { - %= sprintf('(%+d)', $next->{station}{dep_delay}) - % } - % } - % elsif ($next->{type} eq 'present' and $next->{station}{arr}) { - Endstation erreicht um - <strong><%= $next->{station}{arr}->strftime('%H:%M') %></strong> - auf Gleis <strong><%= $next->{station}{platform} %></strong> - % if ($next->{station}{arr_delay}) { - %= sprintf('(%+d)', $next->{station}{arr_delay}) - % } - % } - % elsif ($next->{type} eq 'present') { - Zug steht in - <strong><%= $next->{station}{arr}->strftime('%H:%M') %></strong> - auf Gleis <strong><%= $next->{station}{platform} %></strong> - % } - % elsif ($next->{type} eq 'next' and $next->{station}{arr}) { - Nächster Halt: - <strong><%= $next->{station}{name} %></strong> - um <strong><%= $next->{station}{arr}->strftime('%H:%M') %></strong> - % if ($next->{station}{arr_delay}) { - %= sprintf('(%+d)', $next->{station}{arr_delay}) - % } - auf Gleis <strong><%= $next->{station}{platform} %></strong> - % } - % elsif ($next->{type} eq 'next') { - Nächster Halt: - <strong><%= $next->{station}{name} %></strong> - auf Gleis <strong><%= $next->{station}{platform} %></strong> - % } - % } - </div> +% if (stash('origin') and stash('destination')) { + %= include '_map_infobox' +% } +% elsif (stash('intersection')) { + %= include '_intersection_infobox' % } <div class="container"> @@ -135,9 +85,7 @@ var marker; Die eingezeichnete Route stammt aus dem HAFAS und ist im Detail oft fehlerbehaftet.<br/> Die Zugposition auf der Karte ist eine DBF-eigene Schätzung und kann erheblich -von der tatsächlichen Position des Zugs abweichen.<br/> -Live-Tracking mit automatischer Kartenaktualisierung wird noch nicht -unterstützt. +von den tatsächlichen Gegebenheiten abweichen. </div> % if (my $op = stash('operator')) { |