diff options
Diffstat (limited to 'lib/DBInfoscreen/Controller/Map.pm')
-rw-r--r-- | lib/DBInfoscreen/Controller/Map.pm | 1532 |
1 files changed, 1272 insertions, 260 deletions
diff --git a/lib/DBInfoscreen/Controller/Map.pm b/lib/DBInfoscreen/Controller/Map.pm index 6ab2c32..0a597e1 100644 --- a/lib/DBInfoscreen/Controller/Map.pm +++ b/lib/DBInfoscreen/Controller/Map.pm @@ -1,350 +1,974 @@ package DBInfoscreen::Controller::Map; +# Copyright (C) 2011-2020 Birte Kristina Friesel +# Copyright (C) 2025 networkException <git@nwex.de> +# +# SPDX-License-Identifier: AGPL-3.0-or-later + use Mojo::Base 'Mojolicious::Controller'; -use Mojo::JSON qw(decode_json); +use Mojo::JSON qw(decode_json encode_json); use Mojo::Promise; use DateTime; use DateTime::Format::Strptime; -use Geo::Distance; +use GIS::Distance; +use List::Util qw(); + +my $strp = DateTime::Format::Strptime->new( + pattern => '%Y-%m-%dT%H:%M:%S%z', + time_zone => 'Europe/Berlin', +); + +# Input: +# - polyline: [{lat, lon, name?}, ...] +# - from_name: station name +# - to_name: station name +# Ouptut: +# - from_index: polyline index where name eq from_name +# - to_index: polyline index where name eq to_name +sub get_route_indexes { + my ( $polyline, $from_name, $to_name ) = @_; + my ( $from_index, $to_index ); + + for my $i ( 0 .. $#{$polyline} ) { + my $this_point = $polyline->[$i]; + my $name = $this_point->{name} // $this_point->{stop}->{name}; -my $dbf_version = qx{git describe --dirty} || 'experimental'; + if ( not defined $from_index + and $name + and $name eq $from_name ) + { + $from_index = $i; + } + elsif ( $name + and $name eq $to_name ) + { + $to_index = $i; + last; + } + } + return ( $from_index, $to_index ); +} -chomp $dbf_version; +# Input: +# now: DateTime +# from: current/previous stop +# {arr => DateTime, dep => DateTime, name => str, lat => float, lon => float} +# to: next stop +# {arr => DateTime, dep => DateTime, name => str, lat => float, lon => float} +# route: Travel::Status::DE::HAFAS::Journey->route +# polyline: Travel::Status::DE::HAFAS::Journey->polyline (list of lon/lat hashes) +# 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 ( $self, %opt ) = @_; -sub get_hafas_polyline_p { - my ( $self, $trip_id, $line ) = @_; + my $now = $opt{now}; - my $url - = "https://2.db.transport.rest/trips/${trip_id}?lineName=${line}&polyline=true"; - my $cache = $self->app->cache_iris_rt; - my $promise = Mojo::Promise->new; + my $from_dt = $opt{from}{dep} // $opt{from}{arr}; + my $to_dt = $opt{to}{arr} // $opt{to}{dep}; + my $from_name = $opt{from}{name}; + my $to_name = $opt{to}{name}; + my $route = $opt{route}; + my $polyline = $opt{polyline}; - if ( my $content = $cache->thaw($url) ) { - $promise->resolve($content); - return $promise; + 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 $distance = GIS::Distance->new; + + my ( $from_index, $to_index ) + = get_route_indexes( $polyline, $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 = $polyline->[ $j - 1 ]; + my $this = $polyline->[$j]; + if ( $prev and $this ) { + $total_distance + += $distance->distance_metal( $prev->{lat}, $prev->{lon}, + $this->{lat}, $this->{lon} ); + } + } + my @marker_distances = map { $total_distance * $_ } @completion_ratios; + $total_distance = 0; + for my $j ( $from_index + 1 .. $to_index ) { + my $prev = $polyline->[ $j - 1 ]; + my $this = $polyline->[$j]; + if ( $prev and $this ) { + my $prev_distance = $total_distance; + $total_distance + += $distance->distance_metal( $prev->{lat}, $prev->{lon}, + $this->{lat}, $this->{lon} ); + 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->{lat} + + ( $this->{lat} - $prev->{lat} ) * $sub_ratio; + my $lon = $prev->{lon} + + ( $this->{lon} - $prev->{lon} ) * $sub_ratio; + + push( @train_positions, [ $lat, $lon ] ); + } + } + if ( @train_positions == @completion_ratios ) { + return @train_positions; + } + } + } + if (@train_positions) { + return @train_positions; + } } + else { + $self->log->debug( + "Did not find route indexes for $from_name → $to_name"); + $self->log->debug( +"Falling back to $opt{from}{lat} $opt{from}{lon} → $opt{to}{lat} $opt{to}{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: arrayref of hashrefs +# lat: float +# lon: float +# name: str +# arr: DateTime +# dep: DateTime +# arr_delay: int +# dep_delay: int +# polyline: ref to Travel::Status::DE::HAFAS::Journey polyline list +# Output: +# next_stop: {type, station} +# positions: [current position [lat, lon], 2s from now, 4s from now, ...] +sub estimate_train_positions2 { + my ( $self, %opt ) = @_; + my $now = $opt{now}; + my @route = @{ $opt{route} // [] }; - $self->ua->request_timeout(5) - ->get_p( - $url => { 'User-Agent' => "dbf.finalrewind.org/${dbf_version}" } ) - ->then( + my @train_positions; + my $next_stop; + my $distance = GIS::Distance->new; + my $stop_distance_sum = 0; + my $avg_inter_stop_beeline = 0; + + for my $i ( 1 .. $#route ) { + if ( not $next_stop + and ( $route[$i]{arr} // $route[$i]{dep} ) + and ( $route[ $i - 1 ]{dep} // $route[ $i - 1 ]{arr} ) + and $now > ( $route[ $i - 1 ]{dep} // $route[ $i - 1 ]{arr} ) + and $now < ( $route[$i]{arr} // $route[$i]{dep} ) ) + { + + # HAFAS does not provide delays for past stops + $self->backpropagate_delay( $route[ $i - 1 ], $route[$i] ); + + # (current position, future positons...) in 2 second steps + @train_positions = $self->estimate_train_positions( + from => $route[ $i - 1 ], + to => $route[$i], + now => $now, + route => $opt{route}, + polyline => $opt{polyline}, + ); + + $next_stop = { + type => 'next', + station => $route[$i], + }; + } + if ( not $next_stop + and ( $route[ $i - 1 ]{dep} // $route[ $i - 1 ]{arr} ) + and $now <= ( $route[ $i - 1 ]{dep} // $route[ $i - 1 ]{arr} ) ) + { + @train_positions + = ( [ $route[ $i - 1 ]{lat}, $route[ $i - 1 ]{lon} ] ); + $next_stop = { + type => 'present', + station => $route[ $i - 1 ], + }; + } + $stop_distance_sum += $distance->distance_metal( + $route[ $i - 1 ]{lat}, $route[ $i - 1 ]{lon}, + $route[$i]{lat}, $route[$i]{lon} + ) / 1000; + } + + if ($#route) { + $avg_inter_stop_beeline = $stop_distance_sum / $#route; + } + + if ( @route and 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, + avg_inter_stop_beeline => $avg_inter_stop_beeline, + position_now => $position_now, + positions => \@train_positions, + }; +} + +# input: [{ +# name, platform, +# arr, arr_cancelled, arr_delay, +# dep, dep_cancelled, dep_delay +# }] +sub route_to_ajax { + my (@stopovers) = @_; + + my @route_entries; + + for my $stop (@stopovers) { + my @stop_entries = ( $stop->{name} ); + my $platform; + + if ( my $arr = $stop->{arr} and not $stop->{arr_cancelled} ) { + my $delay = $stop->{arr_delay} // 0; + $platform = $stop->{platform}; + + push( @stop_entries, $arr->epoch, $delay ); + } + else { + push( @stop_entries, q{}, q{} ); + } + + if ( my $dep = $stop->{dep} and not $stop->{dep_cancelled} ) { + my $delay = $stop->{dep_delay} // 0; + $platform //= $stop->{platform} // 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 ); +} + +sub polyline_to_line_pairs { + my (@polyline) = @_; + my @line_pairs; + for my $i ( 1 .. $#polyline ) { + push( + @line_pairs, + [ + [ $polyline[ $i - 1 ]{lat}, $polyline[ $i - 1 ]{lon} ], + [ $polyline[$i]{lat}, $polyline[$i]{lon} ] + ] + ); + } + return @line_pairs; +} + +sub backpropagate_delay { + my ( $self, $prev_stop, $next_stop ) = @_; + + if ( ( $next_stop->{arr_delay} || $next_stop->{dep_delay} ) + and not( $prev_stop->{dep_delay} || $prev_stop->{arr_delay} ) ) + { + $self->log->debug("need to back-propagate delay"); + my $delay = $next_stop->{arr_delay} || $next_stop->{dep_delay}; + if ( $prev_stop->{arr} ) { + $prev_stop->{arr}->add( minutes => $delay ); + $prev_stop->{arr_delay} = $delay; + } + if ( $prev_stop->{dep} ) { + $prev_stop->{dep}->add( minutes => $delay ); + $prev_stop->{dep_delay} = $delay; + } + } +} + +sub route_efa { + my ($self) = @_; + my $trip_id = $self->stash('tripid'); + my $backend = $self->param('efa'); + + my $stopseq; + if ( $trip_id + =~ m{ ^ ([^@]*) @ ([^@]*) [(] ([^T]*) T ([^)]*) [)] (.*) $ }x ) + { + $stopseq = { + stateless => $1, + stop_id => $2, + date => $3, + time => $4, + key => $5 + }; + } + else { + $self->render( + 'route_map', + title => "DBF", + hide_opts => 1, + with_map => 1, + error => "cannot parse trip ID: $trip_id", + ); + return; + } + + $self->efa->get_polyline_p( + stopseq => $stopseq, + service => $backend, + )->then( sub { - my ($tx) = @_; - my $json = decode_json( $tx->res->body ); - my @coordinate_list; + my ($trip) = @_; + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + my @markers; + my @polyline = $trip->polyline( fallback => 1 ); + my @line_pairs = polyline_to_line_pairs(@polyline); + my @route = $trip->route; + + my $ref_route = [ + map { + { + name => $_->full_name, + platform => $_->platform, + arr => $_->arr, + dep => $_->dep, + arr_delay => $_->arr_delay, + dep_delay => $_->dep_delay, + lat => $_->latlon->[0], + lon => $_->latlon->[1] + } + } @route + ]; + + for my $pl (@polyline) { + if ( $pl->{stop} ) { + $pl->{name} = $pl->{stop}->full_name; + } + } + + my $train_pos = $self->estimate_train_positions2( + now => $now, + route => $ref_route, + polyline => \@polyline, + ); - for my $feature ( @{ $json->{polyline}{features} } ) { - if ( exists $feature->{geometry}{coordinates} ) { - push( @coordinate_list, $feature->{geometry}{coordinates} ); + my @station_coordinates; + for my $stop (@route) { + my @stop_lines = ( $stop->full_name ); + 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->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 ($feature->{type} eq 'Feature') { - # say "Feature " . $feature->{properties}{name}; - #} + push( @station_coordinates, [ $stop->latlon, [@stop_lines], ] ); } - my $ret = { - name => $json->{line}{name} // '?', - polyline => [@coordinate_list], - raw => $json, - }; + push( + @markers, + { + lat => $train_pos->{position_now}[0], + lon => $train_pos->{position_now}[1], + title => $trip->name, + } + ); - $cache->freeze( $url, $ret ); - $promise->resolve($ret); + $self->render( + 'route_map', + description => "Karte für " . $trip->name, + title => $trip->name, + hide_opts => 1, + with_map => 1, + ajax_req => "${trip_id}/0", + ajax_route => route_to_ajax( @{$ref_route} ), + ajax_polyline => join( '|', + map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ), + origin => { + name => ( $trip->route )[0]->full_name, + ts => ( $trip->route )[0]->dep, + }, + destination => { + name => ( $trip->route )[-1]->full_name, + ts => ( $trip->route )[-1]->arr, + }, + train_no => $trip->number + ? ( $trip->type // q{} . ' ' . $trip->number ) + : undef, + operator => $trip->operator, + next_stop => $train_pos->{next_stop}, + polyline_groups => [ + { + polylines => \@line_pairs, + color => '#00838f', + opacity => 0.6, + fit_bounds => 1, + } + ], + station_coordinates => \@station_coordinates, + station_radius => 100, + markers => \@markers, + ); } )->catch( sub { my ($err) = @_; - $promise->reject($err); + $self->render( + 'route_map', + title => "DBF", + hide_opts => 1, + with_map => 1, + error => $err, + ); } )->wait; - - return $promise; } -sub route { - my ($self) = @_; +sub route_dbris { + my ($self) = @_; my $trip_id = $self->stash('tripid'); - my $line_no = $self->stash('lineno'); my $from_name = $self->param('from'); my $to_name = $self->param('to'); - $self->render_later; - - $self->get_hafas_polyline_p( $trip_id, $line_no )->then( + $self->dbris->get_polyline_p( id => $trip_id )->then( sub { - my ($pl) = @_; + my ($journey) = @_; - my @polyline = @{ $pl->{polyline} }; - my @line_pairs; + my @polyline = $journey->polyline; 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 = $journey->route; - if ( $from_name and $stop->{stop}{name} eq $from_name ) { + my $train_pos = $self->estimate_train_positions2( + now => $now, + route => [ + map { + { + name => $_->name, + arr => $_->arr, + dep => $_->dep, + arr_delay => $_->arr_delay, + dep_delay => $_->dep_delay, + lat => $_->lat, + lon => $_->lon + } + } @route + ], + polyline => \@polyline, + ); + + # 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}, - icon => 'greenIcon', + 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}, - icon => 'goldIcon', + 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->lat, $stop->lon ], [@stop_lines], ] ); + } + + push( + @markers, + { + lat => $train_pos->{position_now}[0], + lon => $train_pos->{position_now}[1], + title => $journey->train, } + ); - push( - @station_coordinates, - [ - [ - $stop->{stop}{location}{latitude}, - $stop->{stop}{location}{longitude} - ], - [@stop_lines], - ] - ); - push( - @route, + $self->render( + 'route_map', + description => "Karte für " . $journey->train, + title => $journey->train, + hide_opts => 1, + with_map => 1, + ajax_req => "${trip_id}/0", + ajax_route => route_to_ajax( + map { + { + name => $_->name, + platform => $_->platform, + arr => $_->arr, + arr_cancelled => $_->is_cancelled, + arr_delay => $_->arr_delay, + dep => $_->dep, + dep_cancelled => $_->is_cancelled, + dep_delay => $_->dep_delay, + } + } $journey->route + ), + ajax_polyline => join( + '|', + map { join( ';', @{$_} ) } @{ $train_pos->{positions} } + ), + origin => { + name => ( $journey->route )[0]->name, + ts => ( $journey->route )[0]->dep, + }, + destination => { + name => ( $journey->route )[-1]->name, + ts => ( $journey->route )[-1]->arr, + }, + train_no => $journey->number + ? ( $journey->type // q{} . ' ' . $journey->number ) + : undef, + next_stop => $train_pos->{next_stop}, + polyline_groups => [ { - 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, + polylines => [@line_pairs], + color => '#00838f', + opacity => 0.6, + fit_bounds => 1, } - ); + ], + station_coordinates => [@station_coordinates], + station_radius => + ( $train_pos->{avg_inter_stop_beeline} > 500 ? 250 : 100 ), + markers => [@markers], + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + 'route_map', + title => "DBF", + hide_opts => 1, + with_map => 1, + error => $err, + ); - } + } + )->wait; +} - 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} ); - } +sub route_motis { + my ($self) = @_; + + my $service = $self->param('motis') // 'transitous'; + my $trip_id = $self->stash('tripid'); + + my $from_name = $self->param('from'); + my $to_name = $self->param('to'); + + $self->motis->get_polyline_p( + service => $service, + id => $trip_id, + )->then( + sub { + my ($trip) = @_; + + my @polyline = $trip->polyline; + my @station_coordinates; + + my @markers; + + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); - my $geo = Geo::Distance->new; - my ( $from_index, $to_index ); + # used to draw the train's journey on the map + my @line_pairs = polyline_to_line_pairs(@polyline); - 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 ) + my @stopovers = $trip->stopovers; + + my $train_pos = $self->estimate_train_positions2( + now => $now, + route => [ + map { { - $from_index = $j; + name => $_->stop->name, + arr => $_->arrival, + dep => $_->departure, + arr_delay => $_->arrival_delay, + dep_delay => $_->departure_delay, + lat => $_->stop->lat, + lon => $_->stop->lon, } - elsif ( $this_point->{properties}{type} - and $this_point->{properties}{type} eq 'stop' - and $this_point->{properties}{name} eq $to_name ) + } @stopovers + ], + polyline => \@polyline, + ); + + # Prepare from/to markers and name/time/delay overlays for stations + for my $stopover (@stopovers) { + my $stop = $stopover->stop; + my @stop_lines = ( $stop->name ); + + if ( $from_name and $stop->name eq $from_name ) { + push( + @markers, { - $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] - ); - } + lon => $stop->lon, + lat => $stop->lat, + title => $stop->name, + icon => 'goldIcon', } - 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; - } + ); + } + if ( $to_name and $stop->name eq $to_name ) { + push( + @markers, + { + lon => $stop->lon, + lat => $stop->lat, + title => $stop->name, + icon => 'greenIcon', } + ); + } + + if ( $stopover->track ) { + push( @stop_lines, 'Gleis ' . $stop->track ); + } + if ( $stopover->arrival ) { + my $arr_line + = $stopover->arrival->strftime('Ankunft: %H:%M'); + if ( $stopover->arrival_delay ) { + $arr_line + .= sprintf( ' (%+d)', $stopover->arrival_delay ); } - 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], - }; + push( @stop_lines, $arr_line ); + } + if ( $stopover->departure ) { + my $dep_line + = $stopover->departure->strftime('Abfahrt: %H:%M'); + if ( $stopover->departure_delay ) { + $dep_line + .= sprintf( ' (%+d)', $stopover->departure_delay ); } - last; + push( @stop_lines, $dep_line ); + } + + push( @station_coordinates, + [ [ $stop->lat, $stop->lon ], [@stop_lines], ] ); + } + + push( + @markers, + { + lat => $train_pos->{position_now}[0], + lon => $train_pos->{position_now}[1], + title => $trip->route_name, } - 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} ); + ); + + $self->render( + 'route_map', + description => "Karte für " . $trip->route_name, + title => $trip->route_name, + hide_opts => 1, + with_map => 1, + ajax_req => "${trip_id}/0", + ajax_route => route_to_ajax( + map { + { + name => $_->stop->name, + platform => $_->track, + arr => $_->arrival, + arr_cancelled => $_->is_cancelled, + arr_delay => $_->arrival_delay, + dep => $_->departure, + dep_cancelled => $_->is_cancelled, + dep_delay => $_->departure_delay, + } + } $trip->stopovers + ), + ajax_polyline => join( + '|', + map { join( ';', @{$_} ) } @{ $train_pos->{positions} } + ), + origin => { + name => ( $trip->stopovers )[0]->stop->name, + ts => ( $trip->stopovers )[0]->departure, + }, + destination => { + name => ( $trip->stopovers )[-1]->stop->name, + ts => ( $trip->stopovers )[-1]->arrival, + }, + train_no => undef, # FIXME: Better value? + next_stop => $train_pos->{next_stop}, + polyline_groups => [ + { + polylines => [@line_pairs], + color => '#00838f', + opacity => 0.6, + fit_bounds => 1, } + ], + station_coordinates => [@station_coordinates], + station_radius => + ( $train_pos->{avg_inter_stop_beeline} > 500 ? 250 : 100 ), + markers => [@markers], + ); + } + )->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'); + my $line_no = $self->stash('lineno'); + my $hafas = $self->param('hafas'); + + my $from_name = $self->param('from'); + my $to_name = $self->param('to'); + + $self->render_later; + + if ( $self->param('dbris') ) { + return $self->route_dbris; + } + if ( $self->param('motis') ) { + return $self->route_motis; + } + if ( $self->param('efa') ) { + return $self->route_efa; + } + + my $service = 'ÖBB'; + if ( $hafas + and $hafas ne '1' + and Travel::Status::DE::HAFAS::get_service($hafas) ) + { + $service = $hafas; + } + + $self->hafas->get_polyline_p( + id => $trip_id, + line => $line_no, + service => $service + )->then( + sub { + my ($journey) = @_; + + my @polyline = $journey->polyline; + my @station_coordinates; + + my @markers; + + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + + # used to draw the train's journey on the map + my @line_pairs = polyline_to_line_pairs(@polyline); + + my @route = $journey->route; + + my $train_pos = $self->estimate_train_positions2( + now => $now, + route => [ + map { + { + name => $_->loc->name, + arr => $_->arr, + dep => $_->dep, + arr_delay => $_->arr_delay, + dep_delay => $_->dep_delay, + lat => $_->loc->lat, + lon => $_->loc->lon + } + } @route + ], + polyline => \@polyline, + ); + + # Prepare from/to markers and name/time/delay overlays for stations + for my $stop (@route) { + my @stop_lines = ( $stop->loc->name ); + + if ( $from_name and $stop->loc->name eq $from_name ) { push( @markers, { - lat => $route[ $i - 1 ]{lat}, - lon => $route[ $i - 1 ]{lon}, - title => $title + lon => $stop->loc->lon, + lat => $stop->loc->lat, + title => $stop->loc->name, + icon => 'goldIcon', } ); - $next_stop = { - type => 'present', - station => $route[ $i - 1 ], - }; - last; } - } - if ( not @markers ) { - push( - @markers, - { - lat => $route[-1]{lat}, - lon => $route[-1]{lon}, - title => $route[-1]{name} . ' - Endstation', + if ( $to_name and $stop->loc->name eq $to_name ) { + push( + @markers, + { + lon => $stop->loc->lon, + lat => $stop->loc->lat, + title => $stop->loc->name, + icon => 'greenIcon', + } + ); + } + + 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 ); } - ); - $next_stop = { - type => 'present', - station => $route[-1] - }; + push( @stop_lines, $arr_line ); + } + 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 ); + } + + push( @station_coordinates, + [ [ $stop->loc->lat, $stop->loc->lon ], [@stop_lines], ] ); } + push( + @markers, + { + lat => $train_pos->{position_now}[0], + lon => $train_pos->{position_now}[1], + title => $journey->name + } + ); + $self->render( 'route_map', - title => $pl->{name}, - hide_opts => 1, - with_map => 1, - origin => { - name => $pl->{raw}{origin}{name}, - ts => $pl->{raw}{dep_line} - ? scalar $strp->parse_datetime( $pl->{raw}{departure} ) - : undef, + description => "Karte für " . $journey->name, + title => $journey->name, + hide_opts => 1, + with_map => 1, + ajax_req => "${trip_id}/${line_no}", + ajax_route => route_to_ajax( + map { + { + name => $_->loc->name, + platform => $_->platform, + arr => $_->arr, + arr_cancelled => $_->arr_cancelled, + arr_delay => $_->arr_delay, + dep => $_->dep, + dep_cancelled => $_->dep_cancelled, + dep_delay => $_->dep_delay, + } + } $journey->route + ), + ajax_polyline => join( + '|', + map { join( ';', @{$_} ) } @{ $train_pos->{positions} } + ), + origin => { + name => ( $journey->route )[0]->loc->name, + ts => ( $journey->route )[0]->dep, }, destination => { - name => $pl->{raw}{destination}{name}, - ts => $pl->{raw}{arrival} - ? scalar $strp->parse_datetime( $pl->{raw}{arrival} ) - : undef, + name => $journey->route_end, + ts => ( $journey->route )[-1]->arr, }, - train_no => scalar $pl->{raw}{line}{additionalName}, - next_stop => $next_stop, + train_no => $journey->number + ? ( $journey->type // q{} . ' ' . $journey->number ) + : undef, + operator => $journey->operator, + next_stop => $train_pos->{next_stop}, polyline_groups => [ { polylines => [@line_pairs], @@ -354,7 +978,9 @@ sub route { } ], station_coordinates => [@station_coordinates], - markers => [@markers], + station_radius => + ( $train_pos->{avg_inter_stop_beeline} > 500 ? 250 : 100 ), + markers => [@markers], ); } )->catch( @@ -362,16 +988,402 @@ 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_efa { + my ($self) = @_; + my $backend = $self->param('efa'); + my $trip_id = $self->stash('tripid'); + + my $stopseq; + if ( $trip_id + =~ m{ ^ ([^@]*) @ ([^@]*) [(] ([^T]*) T ([^)]*) [)] (.*) $ }x ) + { + $stopseq = { + stateless => $1, + stop_id => $2, + date => $3, + time => $4, + key => $5 + }; + } + else { + $self->render( + '_error', + error => "cannot parse trip ID: $trip_id", + ); + return; + } + + $self->efa->get_polyline_p( + stopseq => $stopseq, + service => $backend + )->then( + sub { + my ($trip) = @_; + + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + + my @polyline = $trip->polyline( fallback => 1 ); + my @route = $trip->route; + + my $ref_route = [ + map { + { + name => $_->full_name, + platform => $_->platform, + arr => $_->arr, + dep => $_->dep, + arr_delay => $_->arr_delay, + dep_delay => $_->dep_delay, + lat => $_->latlon->[0], + lon => $_->latlon->[1] + } + } @route + ]; + + for my $pl (@polyline) { + if ( $pl->{stop} ) { + $pl->{name} = $pl->{stop}->full_name; + } + } + + my $train_pos = $self->estimate_train_positions2( + now => $now, + route => $ref_route, + polyline => \@polyline, + ); + + $self->render( + '_map_infobox', + ajax_req => "${trip_id}/0", + ajax_route => route_to_ajax( @{$ref_route} ), + ajax_polyline => join( '|', + map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ), + origin => { + name => ( $trip->route )[0]->full_name, + ts => ( $trip->route )[0]->dep, + }, + destination => { + name => ( $trip->route )[-1]->full_name, + ts => ( $trip->route )[-1]->arr, + }, + train_no => $trip->number + ? ( $trip->type // q{} . ' ' . $trip->number ) + : undef, + next_stop => $train_pos->{next_stop}, + ); + } + )->catch( + sub { + sub { + my ($err) = @_; + $self->render( + '_error', + error => $err, + ); + } + } + )->wait; +} + +sub ajax_route_dbris { + my ($self) = @_; + my $trip_id = $self->stash('tripid'); + + $self->dbris->get_polyline_p( id => $trip_id )->then( + sub { + my ($journey) = @_; + + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + + my @route = $journey->route; + my @polyline = $journey->polyline; + + my $train_pos = $self->estimate_train_positions2( + now => $now, + route => [ + map { + { + name => $_->name, + arr => $_->arr, + dep => $_->dep, + arr_delay => $_->arr_delay, + dep_delay => $_->dep_delay, + lat => $_->lat, + lon => $_->lon + } + } @route + ], + polyline => \@polyline, + ); + + $self->render( + '_map_infobox', + ajax_req => "${trip_id}/0", + ajax_route => route_to_ajax( + map { + { + name => $_->name, + platform => $_->platform, + arr => $_->arr, + arr_cancelled => $_->is_cancelled, + arr_delay => $_->arr_delay, + dep => $_->dep, + dep_cancelled => $_->is_cancelled, + dep_delay => $_->dep_delay, + } + } @route + ), + ajax_polyline => join( + '|', + map { join( ';', @{$_} ) } @{ $train_pos->{positions} } + ), + origin => { + name => ( $journey->route )[0]->name, + ts => ( $journey->route )[0]->dep, + }, + destination => { + name => ( $journey->route )[-1]->name, + ts => ( $journey->route )[-1]->arr, + }, + train_no => $journey->number + ? ( $journey->type . ' ' . $journey->number ) + : undef, + next_stop => $train_pos->{next_stop}, + platform_type => q{}, + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + '_error', + error => $err, + ); + } + )->wait; +} + +sub ajax_route_motis { + my ($self) = @_; + + my $service = $self->param('motis') // 'transitous'; + my $trip_id = $self->stash('tripid'); + + $self->motis->get_polyline_p( + service => $service, + id => $trip_id, + )->then( + sub { + my ($trip) = @_; + + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + + my @stopovers = $trip->stopovers; + my @polyline = $trip->polyline; + + my $train_pos = $self->estimate_train_positions2( + now => $now, + route => [ + map { + { + name => $_->stop->name, + arr => $_->arrival, + dep => $_->departure, + arr_delay => $_->arrival_delay, + dep_delay => $_->departure_delay, + lat => $_->stop->lat, + lon => $_->stop->lon, + } + } @stopovers + ], + polyline => \@polyline, + ); + + $self->render( + '_map_infobox', + ajax_req => "${trip_id}/0", + ajax_route => route_to_ajax( + map { + { + name => $_->stop->name, + platform => $_->track, + arr => $_->arrival, + arr_cancelled => $_->is_cancelled, + arr_delay => $_->arrival_delay, + dep => $_->departure, + dep_cancelled => $_->is_cancelled, + dep_delay => $_->departure_delay, + } + } @stopovers + ), + ajax_polyline => join( + '|', + map { join( ';', @{$_} ) } @{ $train_pos->{positions} } + ), + origin => { + name => ( $trip->stopovers )[0]->stop->name, + ts => ( $trip->stopovers )[0]->departure, + }, + destination => { + name => ( $trip->stopovers )[-1]->stop->name, + ts => ( $trip->stopovers )[-1]->arrival, + }, + train_no => undef, # FIXME + next_stop => $train_pos->{next_stop}, + platform_type => q{}, + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + '_error', + error => $err, + ); + } + )->wait; +} + +sub ajax_route { + my ($self) = @_; + + delete $self->stash->{layout}; + + $self->render_later; + + if ( $self->param('dbris') ) { + return $self->ajax_route_dbris; + } + if ( $self->param('motis') ) { + return $self->ajax_route_motis; + } + if ( $self->param('efa') ) { + return $self->ajax_route_efa; + } + + my $trip_id = $self->stash('tripid'); + my $line_no = $self->stash('lineno'); + my $hafas = $self->param('hafas'); + + my $service = 'ÖBB'; + if ( $hafas + and $hafas ne '1' + and Travel::Status::DE::HAFAS::get_service($hafas) ) + { + $service = $hafas; + } + + $self->hafas->get_polyline_p( + id => $trip_id, + line => $line_no, + service => $service + )->then( + sub { + my ($journey) = @_; + + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + + my @route = $journey->route; + my @polyline = $journey->polyline; + + my $train_pos = $self->estimate_train_positions2( + now => $now, + route => [ + map { + { + name => $_->loc->name, + arr => $_->arr, + dep => $_->dep, + arr_delay => $_->arr_delay, + dep_delay => $_->dep_delay, + lat => $_->loc->lat, + lon => $_->loc->lon + } + } @route + ], + polyline => \@polyline, + ); + + $self->render( + '_map_infobox', + ajax_req => "${trip_id}/${line_no}", + ajax_route => route_to_ajax( + map { + { + name => $_->loc->name, + platform => $_->platform, + arr => $_->arr, + arr_cancelled => $_->arr_cancelled, + arr_delay => $_->arr_delay, + dep => $_->dep, + dep_cancelled => $_->dep_cancelled, + dep_delay => $_->dep_delay, + } + } @route + ), + ajax_polyline => join( + '|', + map { join( ';', @{$_} ) } @{ $train_pos->{positions} } + ), + origin => { + name => ( $journey->route )[0]->loc->name, + ts => ( $journey->route )[0]->dep, + }, + destination => { + name => $journey->route_end, + ts => ( $journey->route )[-1]->arr, + }, + train_no => $journey->number + ? ( $journey->type . ' ' . $journey->number ) + : undef, + next_stop => $train_pos->{next_stop}, + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + '_error', + error => $err, + ); + } + )->wait; +} + +sub coverage { + my ($self) = @_; + my $backend = lc( $self->stash('backend') ); + my $service = $self->stash('service'); + + my $coverage = {}; + + if ( $backend eq 'efa' ) { + $coverage = $self->efa->get_coverage($service); + } + elsif ( $backend eq 'hafas' ) { + $coverage = $self->hafas->get_coverage($service); + } + elsif ( $backend eq 'motis' ) { + $coverage = $self->motis->get_coverage($service); + } + + $self->render( + 'coverage_map', + title => "Abdeckung $service", + hide_opts => 1, + with_map => 1, + coverage => encode_json($coverage), + ); +} + 1; |