Commit f9f1eec4 authored by Birte Kristina Friesel's avatar Birte Kristina Friesel
Browse files

Use EVA IDs instead of travelynx-internal station IDs. Not 100% tested yet.

parent c6634b40
Loading
Loading
Loading
Loading
+128 −114
Original line number Diff line number Diff line
@@ -117,7 +117,7 @@ sub startup {
			my ($self) = @_;

           # The "theme" cookie is set client-side if the theme we delivered was
          # changed by dark mode detection or by using the theme switcher). It's
           # changed by dark mode detection or by using the theme switcher. It's
           # not part of Mojolicious' session data (and can't be, due to
           # signing and HTTPOnly), so we need to add it here.
			for my $cookie ( @{ $self->req->cookies } ) {
@@ -207,6 +207,18 @@ sub startup {
		}
	);

	$self->attr(
		station_by_eva => sub {
			my %map;
			for
			  my $station ( Travel::Status::DE::IRIS::Stations::get_stations() )
			{
				$map{ $station->[2] } = $station;
			}
			return \%map;
		}
	);

	$self->helper(
		sendmail => sub {
			state $sendmail = Travelynx::Helper::Sendmail->new(
@@ -286,24 +298,26 @@ sub startup {
				return {
					results => [ $status->results ],
					errstr  => $status->errstr,
					station_ds100 => (
						$status->station ? $status->station->{ds100} : 'undef'
					),
					station_ds100 =>
					  ( $status->station ? $status->station->{ds100} : undef ),
					station_eva =>
					  ( $status->station ? $status->station->{uic} : undef ),
					station_name =>
					  ( $status->station ? $status->station->{name} : 'undef' ),
					  ( $status->station ? $status->station->{name} : undef ),
					related_stations => [ $status->related_stations ],
				};
			}
			elsif ( @station_matches > 1 ) {
				return {
					results => [],
					errstr  => 'Ambiguous station name',
					errstr  => 'Mehrdeutiger Stationsname. Mögliche Eingaben: '
					  . join( q{, }, map { $_->[1] } @station_matches ),
				};
			}
			else {
				return {
					results => [],
					errstr  => 'Unknown station name',
					errstr  => 'Unbekannte Station',
				};
			}
		}
@@ -388,17 +402,11 @@ sub startup {
				train_line          => $opt{train_line},
				train_no            => $opt{train_no},
				train_id            => 'manual',
				checkin_station_id => $self->get_station_id(
					ds100 => $dep_station->[0],
					name  => $dep_station->[1],
				),
				checkin_station_id  => $dep_station->[2],
				checkin_time        => $now,
				sched_departure     => $opt{sched_departure},
				real_departure      => $opt{rt_departure},
				checkout_station_id => $self->get_station_id(
					ds100 => $arr_station->[0],
					name  => $arr_station->[1],
				),
				checkout_station_id => $arr_station->[2],
				sched_arrival       => $opt{sched_arrival},
				real_arrival        => $opt{rt_arrival},
				checkout_time       => $now,
@@ -451,7 +459,7 @@ sub startup {
					if ( $user->{checked_in} or $user->{cancelled} ) {

						if (    $user->{train_id} eq $train_id
							and $user->{dep_ds100} eq $status->{station_ds100} )
							and $user->{dep_eva} eq $status->{station_eva} )
						{
							# checking in twice is harmless
							return ( $train, undef );
@@ -470,10 +478,7 @@ sub startup {
								cancelled => $train->departure_is_cancelled
								? 1
								: 0,
								checkin_station_id => $self->get_station_id(
									ds100 => $status->{station_ds100},
									name  => $status->{station_name}
								),
								checkin_station_id => $status->{station_eva},
								checkin_time =>
								  DateTime->now( time_zone => 'Europe/Berlin' ),
								dep_platform    => $train->platform,
@@ -647,6 +652,8 @@ sub startup {
			$train //= List::Util::first { $_->train_id eq $train_id }
			@{ $status->{results} };

			my $new_checkout_station_id = $status->{station_eva};

          # When a checkout is triggered by a checkin, there is an edge case
          # with related stations.
          # Assume a user travels from A to B1, then from B2 to C. B1 and B2 are
@@ -660,14 +667,15 @@ sub startup {
				$status = $self->get_departures( $station, 120, 180, 1 );
				($train) = List::Util::first { $_->train_id eq $train_id }
				@{ $status->{results} };
				if (    $train
					and $self->app->station_by_eva->{ $train->station_uic } )
				{
					$new_checkout_station_id = $train->station_uic;
				}
			}

			# Store the intended checkout station regardless of this operation's
			# success.
			my $new_checkout_station_id = $self->get_station_id(
				ds100 => $status->{station_ds100},
				name  => $status->{station_name}
			);
			$db->update(
				'in_transit',
				{
@@ -698,6 +706,7 @@ sub startup {

               # Arrival time via IRIS is unknown, so the train probably has not
               # arrived yet. Fall back to HAFAS.
               # TODO support cases where $station is EVA or DS100 code
				if (
					my $station_data
					= List::Util::first { $_->[0] eq $station }
@@ -1012,38 +1021,6 @@ sub startup {
		}
	);

	$self->helper(
		'get_station_id' => sub {
			my ( $self, %opt ) = @_;

			my $res = $self->pg->db->select( 'stations', ['id'],
				{ ds100 => $opt{ds100} } );
			my $res_h = $res->hash;

			if ($res_h) {
				$res->finish;
				return $res_h->{id};
			}

			if ( $opt{readonly} ) {
				return;
			}

			$self->pg->db->insert(
				'stations',
				{
					ds100 => $opt{ds100},
					name  => $opt{name},
				}
			);
			$res = $self->pg->db->select( 'stations', ['id'],
				{ ds100 => $opt{ds100} } );
			my $id = $res->hash->{id};
			$res->finish;
			return $id;
		}
	);

	$self->helper(
		'verify_registration_token' => sub {
			my ( $self, $uid, $token ) = @_;
@@ -1812,9 +1789,9 @@ sub startup {

	$self->helper(
		'get_dbdb_station_p' => sub {
			my ( $self, $ds100 ) = @_;
			my ( $self, $eva ) = @_;

			my $url = "https://lib.finalrewind.org/dbdb/s/${ds100}.json";
			my $url = "https://lib.finalrewind.org/dbdb/s/${eva}.json";

			my $cache   = $self->app->cache_iris_main;
			my $promise = Mojo::Promise->new;
@@ -2038,7 +2015,7 @@ sub startup {

			my $journey = $db->select(
				'in_transit_str',
				[ 'arr_ds100', 'dep_ds100', 'route' ],
				[ 'arr_eva', 'dep_eva', 'route' ],
				{ user_id => $uid }
			)->expand->hash;

@@ -2256,7 +2233,7 @@ sub startup {
			}

			if ($is_departure) {
				$self->get_dbdb_station_p( $journey->{dep_ds100} )->then(
				$self->get_dbdb_station_p( $journey->{dep_eva} )->then(
					sub {
						my ($station_info) = @_;

@@ -2276,8 +2253,8 @@ sub startup {
				)->wait;
			}

			if ( $journey->{arr_ds100} and not $is_departure ) {
				$self->get_dbdb_station_p( $journey->{arr_ds100} )->then(
			if ( $journey->{arr_eva} and not $is_departure ) {
				$self->get_dbdb_station_p( $journey->{arr_eva} )->then(
					sub {
						my ($station_info) = @_;

@@ -2367,17 +2344,7 @@ sub startup {
			my $db        = $opt{db} //= $self->pg->db;
			my $min_count = $opt{min_count} // 3;

			my $dest_id;

			if ( $opt{ds100} ) {
				$dest_id = $self->get_station_id(
					ds100    => $opt{ds100},
					readonly => 1
				);
			}
			else {
				$dest_id = $self->get_latest_dest_id(%opt);
			}
			my $dest_id = $opt{eva} // $self->get_latest_dest_id(%opt);

			if ( not $dest_id ) {
				return;
@@ -2386,14 +2353,13 @@ sub startup {
			my $res = $db->query(
				qq{
					select
					count(stations.name) as count,
					stations.name as dest
					count(checkout_station_id) as count,
					checkout_station_id as dest
					from journeys
					left outer join stations on checkout_station_id = stations.id
					where user_id = ?
					and checkin_station_id = ?
					and real_departure > ?
					group by stations.name
					group by checkout_station_id
					order by count desc;
				},
				$uid,
@@ -2403,6 +2369,10 @@ sub startup {
			my @destinations
			  = $res->hashes->grep( sub { shift->{count} >= $min_count } )
			  ->map( sub                { shift->{dest} } )->each;
			@destinations
			  = grep { $self->app->station_by_eva->{$_} } @destinations;
			@destinations
			  = map { $self->app->station_by_eva->{$_}->[1] } @destinations;
			return @destinations;
		}
	);
@@ -2414,24 +2384,24 @@ sub startup {
			my $uid         = $opt{uid} //= $self->current_user->{id};
			my $use_history = $self->account_use_history($uid);

			my ( $ds100, $exclude_via, $exclude_train_id, $exclude_before );
			my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );

			if ( $opt{ds100} ) {
			if ( $opt{eva} ) {
				if ( $use_history & 0x01 ) {
					$ds100 = $opt{ds100};
					$eva = $opt{eva};
				}
			}
			else {
				if ( $use_history & 0x02 ) {
					my $status = $self->get_user_status;
					$ds100            = $status->{arr_ds100};
					$eva              = $status->{arr_eva};
					$exclude_via      = $status->{dep_name};
					$exclude_train_id = $status->{train_id};
					$exclude_before   = $status->{real_arrival}->epoch;
				}
			}

			if ( not $ds100 ) {
			if ( not $eva ) {
				return;
			}

@@ -2445,7 +2415,7 @@ sub startup {
				return;
			}

			my $stationboard = $self->get_departures( $ds100, 0, 40, 1 );
			my $stationboard = $self->get_departures( $eva, 0, 40, 1 );
			if ( $stationboard->{errstr} ) {
				return;
			}
@@ -2481,9 +2451,9 @@ sub startup {
             # This is easiest to achieve in two separate loops.
             #
             # Note that a cancelled train may still have a matching destination
             # in its route_post, e.g. if it leaves out $ds100 due to
             # in its route_post, e.g. if it leaves out $eva due to
             # unscheduled route changes but continues on schedule afterwards
             # -- so it is only cancelled at $ds100, not on the remainder of
             # -- so it is only cancelled at $eva, not on the remainder of
             # the route. Also note that this specific case is not yet handled
             # properly by the cancellation logic etc.

@@ -2594,11 +2564,11 @@ sub startup {
					type         => $entry->{train_type},
					line         => $entry->{train_line},
					no           => $entry->{train_no},
					from_name    => $entry->{dep_name},
					from_eva     => $entry->{dep_eva},
					checkin_ts   => $entry->{checkin_ts},
					sched_dep_ts => $entry->{sched_dep_ts},
					rt_dep_ts    => $entry->{real_dep_ts},
					to_name      => $entry->{arr_name},
					to_eva       => $entry->{arr_eva},
					checkout_ts  => $entry->{checkout_ts},
					sched_arr_ts => $entry->{sched_arr_ts},
					rt_arr_ts    => $entry->{real_arr_ts},
@@ -2608,6 +2578,19 @@ sub startup {
					user_data    => $entry->{user_data},
				};

				if ( my $station
					= $self->app->station_by_eva->{ $ref->{from_eva} } )
				{
					$ref->{from_ds100} = $station->[0];
					$ref->{from_name}  = $station->[1];
				}
				if ( my $station
					= $self->app->station_by_eva->{ $ref->{to_eva} } )
				{
					$ref->{to_ds100} = $station->[0];
					$ref->{to_name}  = $station->[1];
				}

				if ( $opt{with_datetime} ) {
					$ref->{checkin} = epoch_to_dt( $ref->{checkin_ts} );
					$ref->{sched_departure}
@@ -2775,6 +2758,20 @@ sub startup {

			if ($in_transit) {

				if ( my $station
					= $self->app->station_by_eva->{ $in_transit->{dep_eva} } )
				{
					$in_transit->{dep_ds100} = $station->[0];
					$in_transit->{dep_name}  = $station->[1];
				}
				if ( $in_transit->{arr_eva}
					and my $station
					= $self->app->station_by_eva->{ $in_transit->{arr_eva} } )
				{
					$in_transit->{arr_ds100} = $station->[0];
					$in_transit->{arr_name}  = $station->[1];
				}

				my @route = @{ $in_transit->{route} // [] };
				my @route_after;
				my $dep_info;
@@ -2791,14 +2788,16 @@ sub startup {
					if ($is_after) {
						push( @route_after, $station );
					}
					if ( $station->[0] eq $in_transit->{dep_name} ) {
					if (    $in_transit->{dep_name}
						and $station->[0] eq $in_transit->{dep_name} )
					{
						$is_after = 1;
						if ( @{$station} > 1 ) {
							$dep_info = $station->[1];
						}
					}
				}
				my $stop_after_dep = $route_after[0][0];
				my $stop_after_dep = @route_after ? $route_after[0][0] : undef;

				my $ts = $in_transit->{checkout_ts}
				  // $in_transit->{checkin_ts};
@@ -2818,11 +2817,13 @@ sub startup {
					  epoch_to_dt( $in_transit->{sched_dep_ts} ),
					real_departure => epoch_to_dt( $in_transit->{real_dep_ts} ),
					dep_ds100      => $in_transit->{dep_ds100},
					dep_eva        => $in_transit->{dep_eva},
					dep_name       => $in_transit->{dep_name},
					dep_platform   => $in_transit->{dep_platform},
					sched_arrival => epoch_to_dt( $in_transit->{sched_arr_ts} ),
					real_arrival  => epoch_to_dt( $in_transit->{real_arr_ts} ),
					arr_ds100     => $in_transit->{arr_ds100},
					arr_eva       => $in_transit->{arr_eva},
					arr_name      => $in_transit->{arr_name},
					arr_platform  => $in_transit->{arr_platform},
					route_after   => \@route_after,
@@ -2998,6 +2999,18 @@ sub startup {
			if ($latest) {
				my $ts          = $latest->{checkout_ts};
				my $action_time = epoch_to_dt($ts);
				if ( my $station
					= $self->app->station_by_eva->{ $latest->{dep_eva} } )
				{
					$latest->{dep_ds100} = $station->[0];
					$latest->{dep_name}  = $station->[1];
				}
				if ( my $station
					= $self->app->station_by_eva->{ $latest->{arr_eva} } )
				{
					$latest->{arr_ds100} = $station->[0];
					$latest->{arr_name}  = $station->[1];
				}
				return {
					checked_in      => 0,
					cancelled       => 0,
@@ -3011,11 +3024,13 @@ sub startup {
					sched_departure => epoch_to_dt( $latest->{sched_dep_ts} ),
					real_departure  => epoch_to_dt( $latest->{real_dep_ts} ),
					dep_ds100       => $latest->{dep_ds100},
					dep_eva         => $latest->{dep_eva},
					dep_name        => $latest->{dep_name},
					dep_platform    => $latest->{dep_platform},
					sched_arrival   => epoch_to_dt( $latest->{sched_arr_ts} ),
					real_arrival    => epoch_to_dt( $latest->{real_arr_ts} ),
					arr_ds100       => $latest->{arr_ds100},
					arr_eva         => $latest->{arr_eva},
					arr_name        => $latest->{arr_name},
					arr_platform    => $latest->{arr_platform},
					comment         => $latest->{user_data}{comment},
@@ -3037,6 +3052,8 @@ sub startup {
			my ( $self, $uid ) = @_;
			my $status = $self->get_user_status($uid);

			# TODO simplify lon/lat (can be returned from get_user_status)

			my $ret = {
				deprecated => \0,
				checkedIn  => (
@@ -3046,7 +3063,7 @@ sub startup {
				fromStation => {
					ds100         => $status->{dep_ds100},
					name          => $status->{dep_name},
					uic           => undef,
					uic           => $status->{dep_eva},
					longitude     => undef,
					latitude      => undef,
					scheduledTime => $status->{sched_departure}->epoch || undef,
@@ -3055,7 +3072,7 @@ sub startup {
				toStation => {
					ds100         => $status->{arr_ds100},
					name          => $status->{arr_name},
					uic           => undef,
					uic           => $status->{arr_eva},
					longitude     => undef,
					latitude      => undef,
					scheduledTime => $status->{sched_arrival}->epoch || undef,
@@ -3070,14 +3087,13 @@ sub startup {
				actionTime => $status->{timestamp}->epoch,
			};

			if ( $status->{dep_ds100} ) {
			if ( $status->{dep_eva} ) {
				my @station_descriptions
				  = Travel::Status::DE::IRIS::Stations::get_station(
					$status->{dep_ds100} );
					$status->{dep_eva} );
				if ( @station_descriptions == 1 ) {
					(
						undef, undef,
						$ret->{fromStation}{uic},
						undef, undef, undef,
						$ret->{fromStation}{longitude},
						$ret->{fromStation}{latitude}
					) = @{ $station_descriptions[0] };
@@ -3090,8 +3106,7 @@ sub startup {
					$status->{arr_ds100} );
				if ( @station_descriptions == 1 ) {
					(
						undef, undef,
						$ret->{toStation}{uic},
						undef, undef, undef,
						$ret->{toStation}{longitude},
						$ret->{toStation}{latitude}
					) = @{ $station_descriptions[0] };
@@ -3260,7 +3275,6 @@ sub startup {
	$r->get('/impressum')->to('static#imprint');
	$r->get('/imprint')->to('static#imprint');
	$r->get('/offline')->to('static#offline');
	$r->get('/api/v0/:user_action/:token')->to('api#get_v0');
	$r->get('/api/v1/:user_action/:token')->to('api#get_v1');
	$r->get('/login')->to('account#login_form');
	$r->get('/recover')->to('account#request_password_reset');
+132 −0
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@ package Travelynx::Command::database;
use Mojo::Base 'Mojolicious::Command';

use DateTime;
use Travel::Status::DE::IRIS::Stations;

has description => 'Initialize or upgrade database layout';

@@ -809,6 +810,137 @@ my @migrations = (
			}
		);
	},

	# v18 -> v19
	sub {
		my ($db) = @_;
		say
'Transitioning from travelynx station ID to EVA IDs, this may take a while ...';
		$db->query(
			qq{
				alter table in_transit drop constraint in_transit_checkin_station_id_fkey;
				alter table in_transit drop constraint in_transit_checkout_station_id_fkey;
				alter table journeys drop constraint journeys_checkin_station_id_fkey;
				alter table journeys drop constraint journeys_checkout_station_id_fkey;
			}
		);
		for my $journey ( $db->select( 'in_transit_str', '*' )->hashes->each ) {
			my ($s_dep)
			  = Travel::Status::DE::IRIS::Stations::get_station(
				$journey->{dep_ds100} );
			if ( $s_dep->[1] ne $journey->{dep_name} ) {
				die(
"$s_dep->[0] name mismatch: $s_dep->[1] vs. $journey->{dep_name}"
				);
			}
			my $rows = $db->update(
				'in_transit',
				{ checkin_station_id => $s_dep->[2] },
				{ user_id            => $journey->{user_id} }
			)->rows;
			if ( $rows != 1 ) {
				die(
"Update error at in_transit checkin_station_id UID $journey->{user_id}\n"
				);
			}
			if ( $journey->{arr_ds100} ) {
				my ($s_arr)
				  = Travel::Status::DE::IRIS::Stations::get_station(
					$journey->{arr_ds100} );
				if ( $s_arr->[1] ne $journey->{arr_name} ) {
					die(
"$s_arr->[0] name mismatch: $s_arr->[1] vs. $journey->{arr_name}"
					);
				}
				my $rows = $db->update(
					'in_transit',
					{ checkout_station_id => $s_arr->[2] },
					{ user_id             => $journey->{user_id} }
				)->rows;
				if ( $rows != 1 ) {
					die(
"Update error at in_transit checkout_station_id UID $journey->{user_id}\n"
					);
				}
			}
		}
		for my $journey ( $db->select( 'journeys_str', '*' )->hashes->each ) {
			my ($s_dep)
			  = Travel::Status::DE::IRIS::Stations::get_station(
				$journey->{dep_ds100} );
			my ($s_arr)
			  = Travel::Status::DE::IRIS::Stations::get_station(
				$journey->{arr_ds100} );
			if ( $s_dep->[1] ne $journey->{dep_name} ) {
				die(
"$s_dep->[0] name mismatch: $s_dep->[1] vs. $journey->{dep_name}"
				);
			}
			my $rows = $db->update(
				'journeys',
				{ checkin_station_id => $s_dep->[2] },
				{ id                 => $journey->{journey_id} }
			)->rows;
			if ( $rows != 1 ) {
				die(
"While updating journeys#checkin_station_id for journey $journey->{id}: got $rows rows, expected 1\n"
				);
			}
			if ( $s_arr->[1] ne $journey->{arr_name} ) {
				die(
"$s_arr->[0] name mismatch: $s_arr->[1] vs. $journey->{arr_name}"
				);
			}
			$rows = $db->update(
				'journeys',
				{ checkout_station_id => $s_arr->[2] },
				{ id                  => $journey->{journey_id} }
			)->rows;
			if ( $rows != 1 ) {
				die(
"While updating journeys#checkout_station_id for journey $journey->{id}: got $rows rows, expected 1\n"
				);
			}
		}
		$db->query(
			qq{
				drop view journeys_str;
				drop view in_transit_str;
				create view journeys_str as select
					journeys.id as journey_id, user_id,
					train_type, train_line, train_no, train_id,
					extract(epoch from checkin_time) as checkin_ts,
					extract(epoch from sched_departure) as sched_dep_ts,
					extract(epoch from real_departure) as real_dep_ts,
					checkin_station_id as dep_eva,
					extract(epoch from checkout_time) as checkout_ts,
					extract(epoch from sched_arrival) as sched_arr_ts,
					extract(epoch from real_arrival) as real_arr_ts,
					checkout_station_id as arr_eva,
					cancelled, edited, route, messages, user_data,
					dep_platform, arr_platform
					from journeys
					;
				create or replace view in_transit_str as select
					user_id,
					train_type, train_line, train_no, train_id,
					extract(epoch from checkin_time) as checkin_ts,
					extract(epoch from sched_departure) as sched_dep_ts,
					extract(epoch from real_departure) as real_dep_ts,
					checkin_station_id as dep_eva,
					extract(epoch from checkout_time) as checkout_ts,
					extract(epoch from sched_arrival) as sched_arr_ts,
					extract(epoch from real_arrival) as real_arr_ts,
					checkout_station_id as arr_eva,
					cancelled, route, messages, user_data,
					dep_platform, arr_platform, data
					from in_transit
					;
				drop table stations;
				update schema_version set version = 19;
			}
		);
	},
);

sub setup_db {
+3 −3
Original line number Diff line number Diff line
@@ -23,8 +23,8 @@ sub run {
	{

		my $uid      = $entry->{user_id};
		my $dep      = $entry->{dep_ds100};
		my $arr      = $entry->{arr_ds100};
		my $dep      = $entry->{dep_eva};
		my $arr      = $entry->{arr_eva};
		my $train_id = $entry->{train_id};

		# Note: IRIS data is not always updated in real-time. Both departure and
@@ -72,7 +72,7 @@ sub run {

		eval {
			if (
				$entry->{arr_name}
				$arr
				and ( not $entry->{real_arr_ts}
					or $now->epoch - $entry->{real_arr_ts} < 600 )
			  )
+0 −85
Original line number Diff line number Diff line
@@ -27,91 +27,6 @@ sub documentation {
	$self->render('api_documentation');
}

sub get_v0 {
	my ($self) = @_;

	my $api_action = $self->stash('user_action');
	my $api_token  = $self->stash('token');
	if ( $api_action !~ qr{ ^ (?: status | history | action ) $ }x ) {
		$self->render(
			json => {
				error => 'Invalid action',
			},
		);
		return;
	}
	if ( $api_token !~ qr{ ^ (?<id> \d+ ) - (?<token> .* ) $ }x ) {
		$self->render(
			json => {
				error => 'Malformed token',
			},
		);
		return;
	}
	my $uid = $+{id};
	$api_token = $+{token};
	my $token = $self->get_api_token($uid);
	if ( $api_token ne $token->{$api_action} ) {
		$self->render(
			json => {
				error => 'Invalid token',
			},
		);
		return;
	}
	if ( $api_action eq 'status' ) {
		my $status = $self->get_user_status($uid);

		my @station_descriptions;
		my $station_eva = undef;
		my $station_lon = undef;
		my $station_lat = undef;

		if ( $status->{arr_ds100} // $status->{dep_ds100} ) {
			@station_descriptions
			  = Travel::Status::DE::IRIS::Stations::get_station(
				$status->{arr_ds100} // $status->{dep_ds100} );
		}
		if ( @station_descriptions == 1 ) {
			( undef, undef, $station_eva, $station_lon, $station_lat )
			  = @{ $station_descriptions[0] };
		}
		$self->render(
			json => {
				deprecated => \1,
				checked_in => (
					     $status->{checked_in}
					  or $status->{cancelled}
				) ? \1 : \0,
				station => {
					ds100 => $status->{arr_ds100} // $status->{dep_ds100},
					name  => $status->{arr_name}  // $status->{dep_name},
					uic   => $station_eva,
					longitude => $station_lon,
					latitude  => $station_lat,
				},
				train => {
					type => $status->{train_type},
					line => $status->{train_line},
					no   => $status->{train_no},
				},
				actionTime    => $status->{timestamp}->epoch,
				scheduledTime => $status->{sched_arrival}->epoch
				  || $status->{sched_departure}->epoch,
				realTime => $status->{real_arrival}->epoch
				  || $status->{real_departure}->epoch,
			},
		);
	}
	else {
		$self->render(
			json => {
				error => 'not implemented',
			},
		);
	}
}

sub get_v1 {
	my ($self) = @_;