package DBInfoscreen::Controller::Stationboard; use Mojo::Base 'Mojolicious::Controller'; # Copyright (C) 2011-2020 Daniel Friesel # License: 2-Clause BSD use DateTime; use DateTime::Format::Strptime; use Encode qw(decode encode); 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; use XML::LibXML; use utf8; no if $] >= 5.018, warnings => 'experimental::smartmatch'; my $dbf_version = qx{git describe --dirty} || 'experimental'; chomp $dbf_version; my %default = ( backend => 'iris', mode => 'app', admode => 'deparr', ); sub result_is_train { my ( $result, $train ) = @_; if ( $result->can('train_id') ) { # IRIS if ( $train eq $result->type . ' ' . $result->train_no ) { return 1; } return 0; } else { # HAFAS if ( $train eq $result->type . ' ' . $result->train ) { return 1; } return 0; } } sub result_has_line { my ( $result, @lines ) = @_; my $line = $result->line; if ( List::MoreUtils::any { $line =~ m{^$_} } @lines ) { return 1; } return 0; } sub result_has_platform { my ( $result, @platforms ) = @_; my $platform = ( split( qr{ }, $result->platform // '' ) )[0] // ''; if ( List::MoreUtils::any { $_ eq $platform } @platforms ) { return 1; } return 0; } sub result_has_train_type { my ( $result, @train_types ) = @_; my $train_type = $result->type; if ( List::MoreUtils::any { $train_type =~ m{^$_} } @train_types ) { return 1; } return 0; } sub result_has_via { my ( $result, $via ) = @_; if ( not $result->can('route_post') ) { return 1; } my @route = $result->route_post; if ( List::MoreUtils::any { m{$via}i } @route ) { return 1; } return 0; } sub log_api_access { my $counter = 1; if ( -r $ENV{DBFAKEDISPLAY_STATS} ) { $counter = read_file( $ENV{DBFAKEDISPLAY_STATS} ) + 1; } write_file( $ENV{DBFAKEDISPLAY_STATS}, $counter ); return; } sub get_results_for { my ( $backend, $station, %opt ) = @_; my $data; # Cache::File has UTF-8 problems, so strip it (and any other potentially # problematic chars). my $cache_str = $station; $cache_str =~ tr{[0-9a-zA-Z -]}{}cd; if ( $backend eq 'iris' ) { if ( $ENV{DBFAKEDISPLAY_STATS} ) { log_api_access(); } # requests with DS100 codes should be preferred (they avoid # encoding problems on the IRIS server). However, only use them # if we have an exact match. Ask the backend otherwise. my @station_matches = Travel::Status::DE::IRIS::Stations::get_station($station); if ( @station_matches == 1 ) { $station = $station_matches[0][0]; my $status = Travel::Status::DE::IRIS->new( station => $station, main_cache => $opt{cache_iris_main}, realtime_cache => $opt{cache_iris_rt}, log_dir => $ENV{DBFAKEDISPLAY_XMLDUMP_DIR}, lookbehind => 20, lwp_options => { timeout => 10, agent => 'dbf.finalrewind.org/2' }, %opt ); $data = { results => [ $status->results ], errstr => $status->errstr, station_name => ( $status->station ? $status->station->{name} : $station ), }; } elsif ( @station_matches > 1 ) { $data = { results => [], errstr => 'Ambiguous station name', }; } else { $data = { results => [], errstr => 'Unknown station name', }; } } elsif ( $backend eq 'ris' ) { $data = $opt{cache_hafas}->thaw($cache_str); if ( not $data ) { if ( $ENV{DBFAKEDISPLAY_STATS} ) { log_api_access(); } my $status = Travel::Status::DE::HAFAS->new( station => $station, excluded_mots => [qw[bus ferry ondemand tram u]], lwp_options => { timeout => 10, agent => 'dbf.finalrewind.org/2' }, %opt ); $data = { results => [ $status->results ], errstr => $status->errstr, }; $opt{cache_hafas}->freeze( $cache_str, $data ); } } else { $data = { results => [], errstr => "Backend '$backend' not supported", }; } return $data; } sub handle_request { my ($self) = @_; my $station = $self->stash('station'); 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, ); my $api_version = $backend eq 'iris' ? $Travel::Status::DE::IRIS::VERSION : $Travel::Status::DE::HAFAS::VERSION; $self->stash( departures => [] ); $self->stash( title => 'DBF' ); $self->stash( version => $dbf_version ); if ( defined $station and $station =~ s{ [.] txt $ }{}x ) { $template = 'text'; $self->param( station => $station ); $self->stash( layout => 'text' ); } elsif ( defined $station and $station =~ s{ [.] json $ }{}x ) { $template = 'json'; } elsif ( $template ne 'app' ) { $self->stash( layout => 'legacy' ); } # Historically, there were two JSON APIs: 'json' (undocumented, raw # passthrough of serialized Travel::Status::DE::IRIS::Result / # Travel::Status::DE::DE::HAFAS::Result objects) and 'marudor' # (documented, IRIS only, stable versioned API). The latter was initially # created for marudor.de, but quickly used by other clients as well. # # marudor.de switched to a nodejs IRIS parser in December 2018. As the # 'json' API was not used and the 'marudor' variant is no longer related to # (or used by) marudor.de, it was renamed to 'json'. Many clients won't # notice this for year to come, so we make sure mode=marudor still works as # intended. if ( $template eq 'marudor' ) { $template = 'json'; } if ( not( $template ~~ [qw[app infoscreen json multi single text]] ) ) { $template = 'app'; } $self->param( mode => $template ); if ( not $station ) { $self->render( 'landingpage', show_intro => 1 ); return; } if ( $template eq 'json' ) { $backend = 'iris'; $opt{lookahead} = 120; } if ($with_related) { $opt{with_related} = 1; } my $data = get_results_for( $backend, $station, %opt ); my $errstr = $data->{errstr}; if ( not @{ $data->{results} } and $template eq 'json' ) { $self->handle_no_results_json( $backend, $station, $errstr, $api_version ); return; } 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}, [ 'Außerplanmäßiger Halt in', $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 ] ) ]; 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_later; my $wagonorder_req = Mojo::Promise->new; my $stationinfo_req = Mojo::Promise->new; my $route_req = Mojo::Promise->new; if ( $departure->{wr_link} ) { $self->wagonorder->is_available_p( $result, $departure->{wr_link} ) ->then( sub { # great! return; }, sub { $departure->{wr_link} = undef; return; } )->finally( sub { $wagonorder_req->resolve; return; } )->wait; } else { $wagonorder_req->resolve; } $self->wagonorder->get_stationinfo_p( $result->station_uic )->then( sub { my ($station_info) = @_; my ($platform_number) = ( $result->platform =~ m{(\d+)} ); if ( not defined $platform_number ) { return; } my $platform_info = $station_info->{$platform_number}; if ( not $platform_info ) { return; } my $prev_stop = ( $result->route_pre )[-1]; my $next_stop = ( $result->route_post )[0]; my $direction; if ( $platform_info->{kopfgleis} and $next_stop ) { $direction = $platform_info->{direction} eq 'r' ? 'l' : 'r'; } elsif ( $platform_info->{kopfgleis} ) { $direction = $platform_info->{direction}; } elsif ( $prev_stop and exists $platform_info->{direction_from}{$prev_stop} ) { $direction = $platform_info->{direction_from}{$prev_stop}; } elsif ( $next_stop and exists $platform_info->{direction_from}{$next_stop} ) { $direction = $platform_info->{direction_from}{$next_stop} eq 'r' ? 'l' : 'r'; } if ($direction) { $departure->{direction} = $direction; } return; }, sub { # errors don't matter here return; } )->finally( sub { $stationinfo_req->resolve; return; } )->wait; $self->hafas->get_route_timestamps_p( train => $result )->then( sub { my ( $route_ts, $route_info, $trainsearch ) = @_; $departure->{trip_id} = $trainsearch->{trip_id}; # 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 ); } } )->catch( sub { # nop } )->finally( sub { $route_req->resolve; return; } )->wait; # Defer rendering until all requests have completed Mojo::Promise->all( $wagonorder_req, $stationinfo_req, $route_req )->then( sub { $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, ); } )->wait; } 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) { if ( not( $self->numeric_platform_part( $result->platform ) ~~ \@platforms ) ) { push( @platforms, $self->numeric_platform_part( $result->platform ) ); } } @platforms = sort { $a <=> $b } @platforms; } my %pcnt; @results = grep { $pcnt{ $self->numeric_platform_part( $_->platform ) }++ < 1 } @results; @results = map { $_->[1] } sort { $a->[0] <=> $b->[0] } map { [ $self->numeric_platform_part( $_->platform ), $_ ] } @results; } if ( $backend eq 'iris' and $show_realtime ) { if ( $admode eq 'arr' ) { @results = sort { ( $a->arrival // $a->departure ) <=> ( $b->arrival // $b->departure ) } @results; } else { @results = sort { ( $a->departure // $a->arrival ) <=> ( $b->departure // $b->arrival ) } @results; } } @results = $self->filter_results(@results); for my $result (@results) { my $platform = ( split( qr{ }, $result->platform // '' ) )[0]; my $delay = $result->delay; if ( $backend eq 'iris' and $admode eq 'arr' and not $result->arrival ) { next; } if ( $backend eq 'iris' and $admode eq 'dep' and not $result->departure ) { next; } my ( $info, $moreinfo ); if ( $backend eq 'iris' ) { ( $info, $moreinfo ) = $self->format_iris_result_info( $template, $result ); } else { ( $info, $moreinfo ) = $self->format_hafas_result_info($result); } my $time = $result->time; if ( $backend eq 'iris' ) { # ->time defaults to dep, so we only need to overwrite $time # if we want arrival times if ( $admode eq 'arr' ) { $time = $result->sched_arrival->strftime('%H:%M'); } if ($show_realtime) { if ( ( $admode eq 'arr' and $result->arrival ) or not $result->departure ) { $time = $result->arrival->strftime('%H:%M'); } else { $time = $result->departure->strftime('%H:%M'); } } } if ($hide_low_delay) { if ($info) { $info =~ s{ (?: ca [.] \s* )? [+] [ 1 2 3 4 ] $ }{}x; } if ( $delay and $delay < 5 ) { $delay = undef; } } if ($info) { $info =~ s{ (?: ca [.] \s* )? [+] (\d+) }{Verspätung ca $1 Min.}x; } if ( $template eq 'json' ) { my @json_route = $self->json_route_diff( [ $result->route ], [ $result->sched_route ] ); if ( $apiver eq '1' ) { push( @departures, { delay => $delay, destination => $result->destination, isCancelled => $result->can('is_cancelled') ? $result->is_cancelled : undef, messages => { delay => [ map { { timestamp => $_->[0], text => $_->[1] } } $result->delay_messages ], qos => [ map { { timestamp => $_->[0], text => $_->[1] } } $result->qos_messages ], }, platform => $result->platform, route => \@json_route, scheduledPlatform => $result->sched_platform, time => $time, train => $result->train, via => [ $result->route_interesting(3) ], } ); } elsif ( $apiver eq '2' ) { my ( $delay_arr, $delay_dep, $sched_arr, $sched_dep ); if ( $result->arrival ) { $delay_arr = $result->arrival->subtract_datetime( $result->sched_arrival )->in_units('minutes'); } if ( $result->departure ) { $delay_dep = $result->departure->subtract_datetime( $result->sched_departure )->in_units('minutes'); } if ( $result->sched_arrival ) { $sched_arr = $result->sched_arrival->strftime('%H:%M'); } if ( $result->sched_departure ) { $sched_dep = $result->sched_departure->strftime('%H:%M'); } push( @departures, { delayArrival => $delay_arr, delayDeparture => $delay_dep, destination => $result->destination, isCancelled => $result->can('is_cancelled') ? $result->is_cancelled : undef, messages => { delay => [ map { { timestamp => $_->[0], text => $_->[1] } } $result->delay_messages ], qos => [ map { { timestamp => $_->[0], text => $_->[1] } } $result->qos_messages ], }, platform => $result->platform, route => \@json_route, scheduledPlatform => $result->sched_platform, scheduledArrival => $sched_arr, scheduledDeparture => $sched_dep, train => $result->train, via => [ $result->route_interesting(3) ], } ); } else { # apiver == 3 my ( $delay_arr, $delay_dep, $sched_arr, $sched_dep ); if ( $result->arrival ) { $delay_arr = $result->arrival->subtract_datetime( $result->sched_arrival )->in_units('minutes'); } if ( $result->departure ) { $delay_dep = $result->departure->subtract_datetime( $result->sched_departure )->in_units('minutes'); } if ( $result->sched_arrival ) { $sched_arr = $result->sched_arrival->strftime('%H:%M'); } if ( $result->sched_departure ) { $sched_dep = $result->sched_departure->strftime('%H:%M'); } push( @departures, { delayArrival => $delay_arr, delayDeparture => $delay_dep, destination => $result->destination, isCancelled => $result->can('is_cancelled') ? $result->is_cancelled : undef, messages => { delay => [ map { { timestamp => $_->[0], text => $_->[1] } } $result->delay_messages ], qos => [ map { { timestamp => $_->[0], text => $_->[1] } } $result->qos_messages ], }, platform => $result->platform, route => \@json_route, scheduledPlatform => $result->sched_platform, scheduledArrival => $sched_arr, scheduledDeparture => $sched_dep, train => $result->train, trainClasses => [ $result->classes ], trainNumber => $result->train_no, via => [ $result->route_interesting(3) ], } ); } } elsif ( $template eq 'text' ) { push( @departures, [ sprintf( '%5s %s%s', $result->is_cancelled ? '--:--' : $time, ( $delay and $delay > 0 ) ? q{+} : q{}, $delay || q{} ), $result->train, $result->destination, $platform // q{ } ] ); } elsif ( $backend eq 'iris' ) { push( @departures, { time => $time, sched_arrival => $result->sched_arrival ? $result->sched_arrival->strftime('%H:%M') : undef, sched_departure => $result->sched_departure ? $result->sched_departure->strftime('%H:%M') : undef, arrival => $result->arrival ? $result->arrival->strftime('%H:%M') : undef, departure => $result->departure ? $result->departure->strftime('%H:%M') : undef, train => $result->train, train_type => $result->type // '', train_line => $result->line_no, train_no => $result->train_no, via => [ $result->route_interesting(3) ], destination => $result->destination, origin => $result->origin, platform => $result->platform, scheduled_platform => $result->sched_platform, info => $info, is_cancelled => $result->is_cancelled, departure_is_cancelled => $result->departure_is_cancelled, arrival_is_cancelled => $result->arrival_is_cancelled, messages => { delay => [ map { { timestamp => $_->[0], text => $_->[1] } } $result->delay_messages ], qos => [ map { { timestamp => $_->[0], text => $_->[1] } } $result->qos_messages ], }, moreinfo => $moreinfo, delay => $delay, route_pre => [ $result->route_pre ], route_post => [ $result->route_post ], additional_stops => [ $result->additional_stops ], canceled_stops => [ $result->canceled_stops ], replaced_by => [ map { $_->type . q{ } . $_->train_no } $result->replaced_by ], replacement_for => [ map { $_->type . q{ } . $_->train_no } $result->replacement_for ], wr_link => $result->sched_departure ? $result->sched_departure->strftime('%Y%m%d%H%M') : undef, } ); if ( $self->param('train') ) { $self->render_train( $result, $departures[-1], $data->{station_name} // $self->stash('station') ); return; } } else { push( @departures, { time => $time, train => $result->train, train_type => $result->type, destination => $result->destination, platform => $platform, changed_platform => $result->is_changed_platform, info => $info, is_cancelled => $result->can('is_cancelled') ? $result->is_cancelled : undef, messages => { delay => [], qos => [], }, moreinfo => $moreinfo, delay => $delay, additional_stops => [], canceled_stops => [], replaced_by => [], replacement_for => [], } ); } } if ( $template eq 'json' ) { $self->res->headers->access_control_allow_origin(q{*}); my $json = $self->render_to_string( json => { departures => \@departures, } ); if ($callback) { $self->render( data => "$callback($json);", format => 'json' ); } else { $self->render( data => $json, format => 'json' ); } } elsif ( $template eq 'text' ) { my @line_length; for my $i ( 0 .. $#{ $departures[0] } ) { $line_length[$i] = max map { length( $_->[$i] ) } @departures; } my $output = q{}; for my $departure (@departures) { $output .= sprintf( join( q{ }, ( map { "%-${_}s" } @line_length ) ) . "\n", @{$departure}[ 0 .. $#{$departure} ] ); } $self->render( text => $output, format => 'text', ); } else { my $station_name = $data->{station_name} // $self->stash('station'); $self->render( $template, departures => \@departures, ice_type => $self->app->ice_type_map, version => $dbf_version, title => $via ? "$station_name → $via" : $station_name, refresh_interval => $template eq 'app' ? 0 : 120, hide_opts => $hide_opts, hide_low_delay => $hide_low_delay, show_realtime => $show_realtime, load_marquee => ( $template eq 'single' or $template eq 'multi' ), force_mobile => ( $template eq 'app' ), ); } return; } sub stations_by_coordinates { my $self = shift; my $lon = $self->param('lon'); my $lat = $self->param('lat'); if ( not $lon or not $lat ) { $self->render( json => { error => 'Invalid lon/lat received' } ); } else { my @candidates = map { { ds100 => $_->[0][0], name => $_->[0][1], eva => $_->[0][2], lon => $_->[0][3], lat => $_->[0][4], distance => $_->[1], } } Travel::Status::DE::IRIS::Stations::get_station_by_location( $lon, $lat, 10 ); $self->render( json => { candidates => [@candidates], } ); } } 1;