summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Friesel <derf@finalrewind.org>2022-12-07 19:42:48 +0100
committerDaniel Friesel <derf@finalrewind.org>2022-12-07 19:42:48 +0100
commit2f9ba6e0177dbf641c459a7d89528ed561bff6f8 (patch)
treeee80609fd5aec8daef96bd8cfbe1d41d13eba3e1
parentd791825123607e1aa43268521c6204ec16f12868 (diff)
switch to internal station database; add out-of-service stations for old journeys1.28.0
-rw-r--r--README.md3
-rwxr-xr-xlib/Travelynx.pm132
-rw-r--r--lib/Travelynx/Command/database.pm239
-rwxr-xr-xlib/Travelynx/Controller/Api.pm26
-rw-r--r--lib/Travelynx/Helper/IRIS.pm1
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm116
-rw-r--r--lib/Travelynx/Model/Stations.pm88
-rw-r--r--share/old_stations.json2603
-rw-r--r--templates/changelog.html.ep15
9 files changed, 3035 insertions, 188 deletions
diff --git a/README.md b/README.md
index 7addf0d..07c6e26 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ however this method is untested.
In the project root directory (where `cpanfile` resides), run
```
-carton install
+carton install --deployment
```
and set `PERL5LIB=.../local/lib/perl5` before executing any travelynx
@@ -87,6 +87,7 @@ or not.
```
git pull
+carton install --deployment # if you are using carton: update dependencies
chmod -R a+rX . # only needed if travelynx is running under a different user
if perl index.pl database has-current-schema; then
systemctl reload travelynx
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index d473fd2..8c98f15 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -20,7 +20,6 @@ use List::Util;
use List::UtilsBy qw(uniq_by);
use List::MoreUtils qw(first_index);
use Travel::Status::DE::DBWagenreihung;
-use Travel::Status::DE::IRIS::Stations;
use Travelynx::Helper::DBDB;
use Travelynx::Helper::HAFAS;
use Travelynx::Helper::IRIS;
@@ -29,6 +28,7 @@ use Travelynx::Helper::Traewelling;
use Travelynx::Model::InTransit;
use Travelynx::Model::Journeys;
use Travelynx::Model::JourneyStatsCache;
+use Travelynx::Model::Stations;
use Travelynx::Model::Traewelling;
use Travelynx::Model::Users;
@@ -55,27 +55,6 @@ sub epoch_to_dt {
);
}
-sub get_station {
- my ( $station_name, $exact_match ) = @_;
-
- my @candidates
- = Travel::Status::DE::IRIS::Stations::get_station($station_name);
-
- if ( @candidates == 1 ) {
- if ( not $exact_match ) {
- return $candidates[0];
- }
- if ( $candidates[0][0] eq $station_name
- or $candidates[0][1] eq $station_name
- or $candidates[0][2] eq $station_name )
- {
- return $candidates[0];
- }
- return undef;
- }
- return undef;
-}
-
sub startup {
my ($self) = @_;
@@ -227,19 +206,11 @@ sub startup {
$self->attr(
coordinates_by_station => sub {
my $legacy_names = $self->app->renamed_station;
- my %location;
- for
- my $station ( Travel::Status::DE::IRIS::Stations::get_stations() )
- {
- if ( $station->[3] ) {
- $location{ $station->[1] }
- = [ $station->[4], $station->[3] ];
- }
- }
+ my $location = $self->stations->get_latlon_by_name;
while ( my ( $old_name, $new_name ) = each %{$legacy_names} ) {
- $location{$old_name} = $location{$new_name};
+ $location->{$old_name} = $location->{$new_name};
}
- return \%location;
+ return $location;
}
);
@@ -261,18 +232,6 @@ 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;
- }
- );
-
if ( not $self->app->config->{base_url} ) {
$self->app->log->error(
"travelynx.conf: 'base_url' is missing. Links in maintenance/work/worker-generated E-Mails will be incorrect. This variable was introduced in travelynx 1.22; see examples/travelynx.conf for documentation."
@@ -369,7 +328,7 @@ sub startup {
in_transit => $self->in_transit,
stats_cache => $self->journey_stats_cache,
renamed_station => $self->app->renamed_station,
- station_by_eva => $self->app->station_by_eva,
+ stations => $self->stations,
);
}
);
@@ -410,6 +369,14 @@ sub startup {
);
$self->helper(
+ stations => sub {
+ my ($self) = @_;
+ state $stations
+ = Travelynx::Model::Stations->new( pg => $self->pg );
+ }
+ );
+
+ $self->helper(
users => sub {
my ($self) = @_;
state $users = Travelynx::Model::Users->new( pg => $self->pg );
@@ -457,7 +424,8 @@ sub startup {
my @unknown_stations;
for my $station (@stations) {
- my $station_info = get_station($station);
+ my $station_info
+ = $self->stations->get_by_name( $station );
if ( not $station_info ) {
push( @unknown_stations, $station );
}
@@ -689,7 +657,7 @@ sub startup {
($train) = List::Util::first { $_->train_id eq $train_id }
@{ $status->{results} };
if ( $train
- and $self->app->station_by_eva->{ $train->station_uic } )
+ and $self->stations->get_by_eva( $train->station_uic ) )
{
$new_checkout_station_id = $train->station_uic;
}
@@ -1426,17 +1394,17 @@ sub startup {
if ($in_transit) {
if ( my $station
- = $self->app->station_by_eva->{ $in_transit->{dep_eva} } )
+ = $self->stations->get_by_eva( $in_transit->{dep_eva} ) )
{
- $in_transit->{dep_ds100} = $station->[0];
- $in_transit->{dep_name} = $station->[1];
+ $in_transit->{dep_ds100} = $station->{ds100};
+ $in_transit->{dep_name} = $station->{name};
}
if ( $in_transit->{arr_eva}
and my $station
- = $self->app->station_by_eva->{ $in_transit->{arr_eva} } )
+ = $self->stations->get_by_eva( $in_transit->{arr_eva} ) )
{
- $in_transit->{arr_ds100} = $station->[0];
- $in_transit->{arr_name} = $station->[1];
+ $in_transit->{arr_ds100} = $station->{ds100};
+ $in_transit->{arr_name} = $station->{name};
}
my @route = @{ $in_transit->{route} // [] };
@@ -1664,22 +1632,22 @@ sub startup {
if ( $latest_cancellation and $latest_cancellation->{cancelled} ) {
if (
- my $station = $self->app->station_by_eva->{
+ my $station = $self->stations->get_by_eva(
$latest_cancellation->{dep_eva}
- }
+ )
)
{
- $latest_cancellation->{dep_ds100} = $station->[0];
- $latest_cancellation->{dep_name} = $station->[1];
+ $latest_cancellation->{dep_ds100} = $station->{ds100};
+ $latest_cancellation->{dep_name} = $station->{name};
}
if (
- my $station = $self->app->station_by_eva->{
+ my $station = $self->stations->get_by_eva(
$latest_cancellation->{arr_eva}
- }
+ )
)
{
- $latest_cancellation->{arr_ds100} = $station->[0];
- $latest_cancellation->{arr_name} = $station->[1];
+ $latest_cancellation->{arr_ds100} = $station->{ds100};
+ $latest_cancellation->{arr_name} = $station->{name};
}
}
else {
@@ -1690,16 +1658,16 @@ sub startup {
my $ts = $latest->{checkout_ts};
my $action_time = epoch_to_dt($ts);
if ( my $station
- = $self->app->station_by_eva->{ $latest->{dep_eva} } )
+ = $self->stations->get_by_eva( $latest->{dep_eva} ) )
{
- $latest->{dep_ds100} = $station->[0];
- $latest->{dep_name} = $station->[1];
+ $latest->{dep_ds100} = $station->{ds100};
+ $latest->{dep_name} = $station->{name};
}
if ( my $station
- = $self->app->station_by_eva->{ $latest->{arr_eva} } )
+ = $self->stations->get_by_eva( $latest->{arr_eva} ) )
{
- $latest->{arr_ds100} = $station->[0];
- $latest->{arr_name} = $station->[1];
+ $latest->{arr_ds100} = $station->{ds100};
+ $latest->{arr_name} = $station->{name};
}
return {
checked_in => 0,
@@ -1816,28 +1784,18 @@ sub startup {
}
if ( $status->{dep_eva} ) {
- my @station_descriptions
- = Travel::Status::DE::IRIS::Stations::get_station(
- $status->{dep_eva} );
- if ( @station_descriptions == 1 ) {
- (
- undef, undef, undef,
- $ret->{fromStation}{longitude},
- $ret->{fromStation}{latitude}
- ) = @{ $station_descriptions[0] };
+ if ( my $s = $self->stations->get_by_eva( $status->{dep_eva} ) )
+ {
+ $ret->{fromStation}{longitude} = $s->{lon};
+ $ret->{fromStation}{latitude} = $s->{lat};
}
}
- if ( $status->{arr_ds100} ) {
- my @station_descriptions
- = Travel::Status::DE::IRIS::Stations::get_station(
- $status->{arr_ds100} );
- if ( @station_descriptions == 1 ) {
- (
- undef, undef, undef,
- $ret->{toStation}{longitude},
- $ret->{toStation}{latitude}
- ) = @{ $station_descriptions[0] };
+ if ( $status->{arr_eva} ) {
+ if ( my $s = $self->stations->get_by_eva( $status->{arr_eva} ) )
+ {
+ $ret->{toStation}{longitude} = $s->{lon};
+ $ret->{toStation}{latitude} = $s->{lat};
}
}
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm
index 33612c3..d3a5006 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -6,12 +6,27 @@ package Travelynx::Command::database;
use Mojo::Base 'Mojolicious::Command';
use DateTime;
+use File::Slurp qw(read_file);
+use JSON;
use Travel::Status::DE::IRIS::Stations;
has description => 'Initialize or upgrade database layout';
has usage => sub { shift->extract_usage };
+sub get_iris_version {
+ my ($db) = @_;
+ my $version;
+
+ eval { $version = $db->select( 'schema_version', ['iris'] )->hash->{iris}; };
+ if ($@) {
+
+ # If it failed, the version table does not exist -> run setup first.
+ return undef;
+ }
+ return $version;
+}
+
sub get_schema_version {
my ($db) = @_;
my $version;
@@ -1106,8 +1121,212 @@ my @migrations = (
}
);
},
+
+ # v26 -> v27
+ # add list of stations that are not (or no longer) present in T-S-DE-IRIS
+ # (in this case, stations that were removed up to 1.74)
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table schema_version
+ add column iris varchar(12);
+ create table stations (
+ eva int not null primary key,
+ ds100 varchar(16) not null,
+ name varchar(64) not null,
+ lat real not null,
+ lon real not null,
+ source smallint not null,
+ archived bool not null
+ );
+ update schema_version set version = 27;
+ update schema_version set iris = '0';
+ }
+ );
+ },
);
+sub sync_stations {
+ my ( $db, $iris_version ) = @_;
+
+ $db->update( 'schema_version',
+ { iris => $Travel::Status::DE::IRIS::Stations::VERSION } );
+
+ say 'Updating stations table, this may take a while ...';
+ my $total = scalar Travel::Status::DE::IRIS::Stations::get_stations();
+ my $count = 0;
+ for my $s ( Travel::Status::DE::IRIS::Stations::get_stations() ) {
+ my ( $ds100, $name, $eva, $lon, $lat ) = @{$s};
+ $db->insert(
+ 'stations',
+ {
+ eva => $eva,
+ ds100 => $ds100,
+ name => $name,
+ lat => $lat,
+ lon => $lon,
+ source => 0,
+ archived => 0
+ },
+ {
+ on_conflict => \
+ '(eva) do update set archived = false, source = 0'
+ }
+ );
+ if ( $count++ % 1000 == 0 ) {
+ printf( " %2.0f%% complete\n", $count * 100 / $total );
+ }
+ }
+ say ' done';
+
+ my $res1 = $db->query(
+ qq{
+ select checkin_station_id
+ from journeys
+ left join stations on journeys.checkin_station_id = stations.eva
+ where stations.eva is null
+ limit 1;
+ }
+ )->hash;
+
+ my $res2 = $db->query(
+ qq{
+ select checkout_station_id
+ from journeys
+ left join stations on journeys.checkout_station_id = stations.eva
+ where stations.eva is null
+ limit 1;
+ }
+ )->hash;
+
+ if ( $res1 or $res2 ) {
+ say 'Dropping stats cache for archived stations ...';
+ $db->query('truncate journey_stats;');
+ }
+
+ say 'Updating archived stations ...';
+ my $old_stations
+ = JSON->new->utf8->decode( scalar read_file('share/old_stations.json') );
+ for my $s ( @{$old_stations} ) {
+ $db->insert(
+ 'stations',
+ {
+ eva => $s->{eva},
+ ds100 => $s->{ds100},
+ name => $s->{name},
+ lat => $s->{latlong}[0],
+ lon => $s->{latlong}[1],
+ source => 0,
+ archived => 1
+ },
+ { on_conflict => undef }
+ );
+ }
+
+ if ( $iris_version == 0 ) {
+ say 'Applying EVA ID changes ...';
+ for my $change (
+ [ 721394, 301002, 'RKBP: Kronenplatz (U), Karlsruhe' ],
+ [
+ 721356, 901012,
+ 'RKME: Ettlinger Tor/Staatstheater (U), Karlsruhe'
+ ],
+ )
+ {
+ my ( $old, $new, $desc ) = @{$change};
+ my $rows = $db->update(
+ 'journeys',
+ { checkout_station_id => $new },
+ { checkout_station_id => $old }
+ )->rows;
+ $rows += $db->update(
+ 'journeys',
+ { checkin_station_id => $new },
+ { checkin_station_id => $old }
+ )->rows;
+ if ($rows) {
+ say "$desc ($old -> $new) : $rows rows";
+ }
+ }
+ }
+
+ say 'Checking for unknown EVA IDs ...';
+ my $found = 0;
+
+ $res1 = $db->query(
+ qq{
+ select checkin_station_id
+ from journeys
+ left join stations on journeys.checkin_station_id = stations.eva
+ where stations.eva is null;
+ }
+ );
+
+ $res2 = $db->query(
+ qq{
+ select checkout_station_id
+ from journeys
+ left join stations on journeys.checkout_station_id = stations.eva
+ where stations.eva is null;
+ }
+ );
+
+ my %notified;
+ while ( my $row = $res1->hash ) {
+ my $eva = $row->{checkin_station_id};
+ if ( not $found ) {
+ $found = 1;
+ say '';
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ }
+ if ( not $notified{$eva} ) {
+ say $eva;
+ $notified{$eva} = 1;
+ }
+ }
+
+ while ( my $row = $res2->hash ) {
+ my $eva = $row->{checkout_station_id};
+ if ( not $found ) {
+ $found = 1;
+ say '';
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ }
+ if ( not $notified{$eva} ) {
+ say $eva;
+ $notified{$eva} = 1;
+ }
+ }
+
+ if ($found) {
+ say '------------8<----------';
+ say '';
+ say
+'Due to a conceptual flaw in past travelynx releases, your database contains unknown EVA IDs.';
+ say
+'Please file a bug report titled "Missing EVA IDs after DB migration" at https://github.com/derf/travelynx/issues';
+ say 'and include the list shown above in the bug report.';
+ say
+'If you do not have a GitHub account, please send an E-Mail to derf+travelynx@finalrewind.org instead.';
+ say '';
+ say 'This issue does not affect usability or long-term data integrity,';
+ say 'and handling it is not time-critical.';
+ say
+'Past journeys referencing unknown EVA IDs may have inaccurate distance statistics,';
+ say
+'but this will be resolved once a future release handles those EVA IDs.';
+ say 'Note that this issue was already present in previous releases.';
+ }
+ else {
+ say 'None found.';
+ }
+}
+
sub setup_db {
my ($db) = @_;
my $tx = $db->begin;
@@ -1129,7 +1348,7 @@ sub migrate_db {
say "Found travelynx schema v${schema_version}";
if ( $schema_version == @migrations ) {
- say "Database layout is up-to-date";
+ say 'Database layout is up-to-date';
}
eval {
@@ -1144,6 +1363,24 @@ sub migrate_db {
exit(1);
}
+ my $iris_version = get_iris_version($db);
+ say "Found IRIS station database v${iris_version}";
+ if ( $iris_version eq $Travel::Status::DE::IRIS::Stations::VERSION ) {
+ say 'Station database is up-to-date';
+ }
+ else {
+ eval {
+ say
+"Synchronizing with Travel::Status::DE::IRIS $Travel::Status::DE::IRIS::Stations::VERSION";
+ sync_stations( $db, $iris_version );
+ };
+ if ($@) {
+ say STDERR "Synchronization failed: $@";
+ say STDERR "Rolling back to v${schema_version}";
+ exit(1);
+ }
+ }
+
if ( get_schema_version($db) == @migrations ) {
$tx->commit;
}
diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm
index 3e9c5eb..f686222 100755
--- a/lib/Travelynx/Controller/Api.pm
+++ b/lib/Travelynx/Controller/Api.pm
@@ -7,7 +7,6 @@ use Mojo::Base 'Mojolicious::Controller';
use DateTime;
use List::Util;
-use Travel::Status::DE::IRIS::Stations;
use UUID::Tiny qw(:std);
# Internal Helpers
@@ -184,41 +183,24 @@ sub travel_v1 {
return;
}
- if (
- @{
- [
- Travel::Status::DE::IRIS::Stations::get_station(
- $from_station)
- ]
- } != 1
- )
- {
+ if ( not $self->stations->search($from_station) ) {
$self->render(
json => {
success => \0,
deprecated => \0,
- error => 'fromStation is ambiguous',
+ error => 'Unknown fromStation',
status => $self->get_user_status_json_v1($uid)
},
);
return;
}
- if (
- $to_station
- and @{
- [
- Travel::Status::DE::IRIS::Stations::get_station(
- $to_station)
- ]
- } != 1
- )
- {
+ if ( $to_station and not $self->stations->search($to_station) ) {
$self->render(
json => {
success => \0,
deprecated => \0,
- error => 'toStation is ambiguous',
+ error => 'Unknown toStation',
status => $self->get_user_status_json_v1($uid)
},
);
diff --git a/lib/Travelynx/Helper/IRIS.pm b/lib/Travelynx/Helper/IRIS.pm
index ef179c8..3222dad 100644
--- a/lib/Travelynx/Helper/IRIS.pm
+++ b/lib/Travelynx/Helper/IRIS.pm
@@ -13,6 +13,7 @@ use utf8;
use Mojo::Promise;
use Mojo::UserAgent;
use Travel::Status::DE::IRIS;
+use Travel::Status::DE::IRIS::Stations;
sub new {
my ( $class, %opt ) = @_;
diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm
index a8ca299..3706916 100755
--- a/lib/Travelynx/Model/Journeys.pm
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -6,7 +6,6 @@ package Travelynx::Model::Journeys;
use GIS::Distance;
use List::MoreUtils qw(after_incl before_incl);
-use Travel::Status::DE::IRIS::Stations;
use strict;
use warnings;
@@ -35,33 +34,12 @@ sub epoch_to_dt {
);
}
-sub get_station {
- my ( $station_name, $exact_match ) = @_;
-
- my @candidates
- = Travel::Status::DE::IRIS::Stations::get_station($station_name);
-
- if ( @candidates == 1 ) {
- if ( not $exact_match ) {
- return $candidates[0];
- }
- if ( $candidates[0][0] eq $station_name
- or $candidates[0][1] eq $station_name
- or $candidates[0][2] eq $station_name )
- {
- return $candidates[0];
- }
- return undef;
- }
- return undef;
-}
-
sub grep_unknown_stations {
- my (@stations) = @_;
+ my ( $self, @stations ) = @_;
my @unknown_stations;
for my $station (@stations) {
- my $station_info = get_station($station);
+ my $station_info = $self->{stations}->get_by_name($station);
if ( not $station_info ) {
push( @unknown_stations, $station );
}
@@ -100,8 +78,8 @@ sub add {
my $db = $opt{db};
my $uid = $opt{uid};
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $dep_station = get_station( $opt{dep_station} );
- my $arr_station = get_station( $opt{arr_station} );
+ my $dep_station = $self->{stations}->search( $opt{dep_station} );
+ my $arr_station = $self->{stations}->search( $opt{arr_station} );
if ( not $dep_station ) {
return ( undef, 'Unbekannter Startbahnhof' );
@@ -134,10 +112,14 @@ sub add {
my $route_has_stop = 0;
for my $station ( @{ $opt{route} || [] } ) {
- if ( $station eq $dep_station->[1] or $station eq $dep_station->[0] ) {
+ if ( $station eq $dep_station->{name}
+ or $station eq $dep_station->{ds100} )
+ {
$route_has_start = 1;
}
- if ( $station eq $arr_station->[1] or $station eq $arr_station->[0] ) {
+ if ( $station eq $arr_station->{name}
+ or $station eq $arr_station->{ds100} )
+ {
$route_has_stop = 1;
}
}
@@ -145,15 +127,15 @@ sub add {
my @route;
if ( not $route_has_start ) {
- push( @route, [ $dep_station->[1], {}, undef ] );
+ push( @route, [ $dep_station->{name}, {}, undef ] );
}
if ( $opt{route} ) {
my @unknown_stations;
for my $station ( @{ $opt{route} } ) {
- my $station_info = get_station($station);
+ my $station_info = $self->{stations}->search($station);
if ($station_info) {
- push( @route, [ $station_info->[1], {}, undef ] );
+ push( @route, [ $station_info->{name}, {}, undef ] );
}
else {
push( @route, [ $station, {}, undef ] );
@@ -175,7 +157,7 @@ sub add {
}
if ( not $route_has_stop ) {
- push( @route, [ $arr_station->[1], {}, undef ] );
+ push( @route, [ $arr_station->{name}, {}, undef ] );
}
my $entry = {
@@ -184,11 +166,11 @@ sub add {
train_line => $opt{train_line},
train_no => $opt{train_no},
train_id => 'manual',
- checkin_station_id => $dep_station->[2],
+ checkin_station_id => $dep_station->{eva},
checkin_time => $now,
sched_departure => $opt{sched_departure},
real_departure => $opt{rt_departure},
- checkout_station_id => $arr_station->[2],
+ checkout_station_id => $arr_station->{eva},
sched_arrival => $opt{sched_arrival},
real_arrival => $opt{rt_arrival},
checkout_time => $now,
@@ -252,14 +234,14 @@ sub update {
eval {
if ( exists $opt{from_name} ) {
- my $from_station = get_station( $opt{from_name}, 1 );
+ my $from_station = $self->{stations}->search( $opt{from_name} );
if ( not $from_station ) {
die("Unbekannter Startbahnhof\n");
}
$rows = $db->update(
'journeys',
{
- checkin_station_id => $from_station->[2],
+ checkin_station_id => $from_station->{eva},
edited => $journey->{edited} | 0x0004,
},
{
@@ -268,14 +250,14 @@ sub update {
)->rows;
}
if ( exists $opt{to_name} ) {
- my $to_station = get_station( $opt{to_name}, 1 );
+ my $to_station = $self->{stations}->search( $opt{to_name} );
if ( not $to_station ) {
die("Unbekannter Zielbahnhof\n");
}
$rows = $db->update(
'journeys',
{
- checkout_station_id => $to_station->[2],
+ checkout_station_id => $to_station->{eva},
edited => $journey->{edited} | 0x0400,
},
{
@@ -559,13 +541,13 @@ sub get {
$ref->{polyline} = $entry->{polyline};
}
- if ( my $station = $self->{station_by_eva}->{ $ref->{from_eva} } ) {
- $ref->{from_ds100} = $station->[0];
- $ref->{from_name} = $station->[1];
+ if ( my $station = $self->{stations}->get_by_eva( $ref->{from_eva} ) ) {
+ $ref->{from_ds100} = $station->{ds100};
+ $ref->{from_name} = $station->{name};
}
- if ( my $station = $self->{station_by_eva}->{ $ref->{to_eva} } ) {
- $ref->{to_ds100} = $station->[0];
- $ref->{to_name} = $station->[1];
+ if ( my $station = $self->{stations}->get_by_eva( $ref->{to_eva} ) ) {
+ $ref->{to_ds100} = $station->{ds100};
+ $ref->{to_name} = $station->{name};
}
if ( $opt{with_datetime} ) {
@@ -938,7 +920,8 @@ sub sanity_check {
}
if ( $journey->{edited} & 0x0010 and not $lax ) {
my @unknown_stations
- = grep_unknown_stations( map { $_->[0] } @{ $journey->{route} } );
+ = $self->grep_unknown_stations( map { $_->[0] }
+ @{ $journey->{route} } );
if (@unknown_stations) {
return 'Unbekannte Station(en): ' . join( ', ', @unknown_stations );
}
@@ -989,8 +972,6 @@ sub get_travel_distance {
my $prev_station = shift @polyline;
for my $station (@polyline) {
-
- #lonlatlonlat
$distance_polyline += $geo->distance_metal(
$prev_station->[1], $prev_station->[0],
$station->[1], $station->[0]
@@ -998,45 +979,30 @@ sub get_travel_distance {
$prev_station = $station;
}
- $prev_station = get_station( shift @route );
+ $prev_station = $self->{stations}->get_by_name( shift @route );
if ( not $prev_station ) {
return ( $distance_polyline, 0, 0 );
}
- # Geo-coordinates for stations outside Germany are not available
- # at the moment. When calculating distance with intermediate stops,
- # these are simply left out (as if they were not part of the route).
- # For beeline distance calculation, we use the route's first and last
- # station with known geo-coordinates.
my $from_station_beeline;
my $to_station_beeline;
- # $#{$station} >= 4 iff $station has geocoordinates
for my $station_name (@route) {
- if ( my $station = get_station($station_name) ) {
- if ( not $from_station_beeline and $#{$prev_station} >= 4 ) {
- $from_station_beeline = $prev_station;
- }
- if ( $#{$station} >= 4 ) {
- $to_station_beeline = $station;
- }
- if ( $#{$prev_station} >= 4 and $#{$station} >= 4 ) {
- $distance_intermediate += $geo->distance_metal(
- $prev_station->[4], $prev_station->[3],
- $station->[4], $station->[3]
- );
- }
- else {
- $skipped++;
- }
+ if ( my $station = $self->{stations}->get_by_name($station_name) ) {
+ $from_station_beeline //= $prev_station;
+ $to_station_beeline = $station;
+ $distance_intermediate += $geo->distance_metal(
+ $prev_station->{lat}, $prev_station->{lon},
+ $station->{lat}, $station->{lon}
+ );
$prev_station = $station;
}
}
if ( $from_station_beeline and $to_station_beeline ) {
$distance_beeline = $geo->distance_metal(
- $from_station_beeline->[4], $from_station_beeline->[3],
- $to_station_beeline->[4], $to_station_beeline->[3]
+ $from_station_beeline->{lat}, $from_station_beeline->{lon},
+ $to_station_beeline->{lat}, $to_station_beeline->{lon}
);
}
@@ -1264,10 +1230,8 @@ sub get_connection_targets {
my @destinations
= $res->hashes->grep( sub { shift->{count} >= $min_count } )
->map( sub { shift->{dest} } )->each;
- @destinations
- = grep { $self->{station_by_eva}{$_} } @destinations;
- @destinations
- = map { $self->{station_by_eva}{$_}->[1] } @destinations;
+ @destinations = $self->{stations}->get_by_evas(@destinations);
+ @destinations = map { $_->{name} } @destinations;
return @destinations;
}
diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm
new file mode 100644
index 0000000..6c898b1
--- /dev/null
+++ b/lib/Travelynx/Model/Stations.pm
@@ -0,0 +1,88 @@
+package Travelynx::Model::Stations;
+
+# Copyright (C) 2022 Daniel Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ return bless( \%opt, $class );
+}
+
+# Fast
+sub get_by_eva {
+ my ( $self, $eva, %opt ) = @_;
+
+ if ( not $eva ) {
+ return;
+ }
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ return $db->select( 'stations', '*', { eva => $eva } )->hash;
+}
+
+# Fast
+sub get_by_evas {
+ my ( $self, @evas ) = @_;
+
+ my @ret
+ = $self->{pg}->db->select( 'stations', '*', { eva => { '=', \@evas } } )
+ ->hashes->each;
+ return @ret;
+}
+
+# Slow
+sub get_latlon_by_name {
+ my ( $self, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my %location;
+ my $res = $db->select( 'stations', [ 'name', 'lat', 'lon' ] );
+ while ( my $row = $res->hash ) {
+ $location{ $row->{name} } = [ $row->{lat}, $row->{lon} ];
+ }
+ return \%location;
+}
+
+# Slow
+sub get_by_name {
+ my ( $self, $name, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ return $db->select( 'stations', '*', { name => $name }, { limit => 1 } )
+ ->hash;
+}
+
+# Slow
+sub get_by_ds100 {
+ my ( $self, $ds100, %opt ) = @_;
+
+ my $db = $opt{db} // $self->{pg}->db;
+
+ return $db->select( 'stations', '*', { ds100 => $ds100 }, { limit => 1 } )
+ ->hash;
+}
+
+# Can be slow
+sub search {
+ my ( $self, $identifier, %opt ) = @_;
+
+ if ( $identifier =~ m{ ^ \d+ $ }x ) {
+ return $self->get_by_eva( $identifier, %opt )
+ // $self->get_by_ds100( $identifier, %opt )
+ // $self->get_by_name( $identifier, %opt );
+ }
+
+ return $self->get_by_ds100( $identifier, %opt )
+ // $self->get_by_name( $identifier, %opt );
+}
+
+1;
diff --git a/share/old_stations.json b/share/old_stations.json
new file mode 100644
index 0000000..b3831e7
--- /dev/null
+++ b/share/old_stations.json
@@ -0,0 +1,2603 @@
+[
+ {
+ "ds100" : "AARF",
+ "eva" : 8071338,
+ "latlong" : [
+ 53.233409,
+ 8.818139
+ ],
+ "name" : "Ahrensfelde(Bz. Bremen)"
+ },
+ {
+ "ds100" : "XIAE",
+ "eva" : 8300358,
+ "latlong" : [
+ 43.8707,
+ 7.553207
+ ],
+ "name" : "Airole"
+ },
+ {
+ "ds100" : "XIAO",
+ "eva" : 8300133,
+ "latlong" : [
+ 44.007929,
+ 8.171387
+ ],
+ "name" : "Alassio"
+ },
+ {
+ "ds100" : "XUAI",
+ "eva" : 5300096,
+ "latlong" : [
+ 46.0563473,
+ 23.576231
+ ],
+ "name" : "Alba Iulia"
+ },
+ {
+ "ds100" : "XIAT",
+ "eva" : 8300950,
+ "latlong" : [
+ 45.783184,
+ 9.079832
+ ],
+ "name" : "Albate-Camerlata"
+ },
+ {
+ "ds100" : "XIAB",
+ "eva" : 8300132,
+ "latlong" : [
+ 44.047308,
+ 8.221556
+ ],
+ "name" : "Albenga"
+ },
+ {
+ "ds100" : "FALH",
+ "eva" : 8070252,
+ "latlong" : [
+ 49.644776,
+ 8.096752
+ ],
+ "name" : "Albisheim(Pfrimm)"
+ },
+ {
+ "ds100" : "HAHS",
+ "eva" : 8000486,
+ "latlong" : [
+ 52.50347141,
+ 7.964338
+ ],
+ "name" : "Alfhausen"
+ },
+ {
+ "ds100" : "MAF",
+ "eva" : 8026354,
+ "latlong" : [
+ 48.554979,
+ 12.102813
+ ],
+ "name" : "Altdorf(Niederbay)"
+ },
+ {
+ "ds100" : "DAMG",
+ "eva" : 8070697,
+ "latlong" : [
+ 51.237787,
+ 13.040356
+ ],
+ "name" : "Altmügeln"
+ },
+ {
+ "ds100" : "TAM M",
+ "eva" : 8079075,
+ "latlong" : [
+ 48.577852,
+ 9.873959
+ ],
+ "name" : "Amstetten(W) Lokalbahn"
+ },
+ {
+ "ds100" : "XUA",
+ "eva" : 5300002,
+ "latlong" : [
+ 46.189565,
+ 21.325546
+ ],
+ "name" : "Arad"
+ },
+ {
+ "ds100" : "XSABS",
+ "eva" : 8506110,
+ "latlong" : [
+ 47.5126421316245,
+ 9.44032100861396
+ ],
+ "name" : "Arbon (See)"
+ },
+ {
+ "ds100" : "XIAZ",
+ "eva" : 8300179,
+ "latlong" : [
+ 43.461078,
+ 11.875385
+ ],
+ "name" : "Arezzo"
+ },
+ {
+ "ds100" : "XKA",
+ "eva" : 7000107,
+ "latlong" : [
+ 51.143431,
+ 0.875153
+ ],
+ "name" : "Ashford(Kent)"
+ },
+ {
+ "ds100" : "XKAI",
+ "eva" : 7098107,
+ "latlong" : [
+ 51.143256,
+ 0.874723
+ ],
+ "name" : "Ashford(Kent) Int."
+ },
+ {
+ "ds100" : "UAS",
+ "eva" : 8011060,
+ "latlong" : [
+ 51.10203,
+ 11.590437
+ ],
+ "name" : "Auerstedt"
+ },
+ {
+ "ds100" : "LDU",
+ "eva" : 8011070,
+ "latlong" : [
+ 51.587623,
+ 12.598815
+ ],
+ "name" : "Bad Düben(Mulde)"
+ },
+ {
+ "ds100" : "MBEP",
+ "eva" : 8000700,
+ "latlong" : [
+ 47.884007,
+ 12.632888
+ ],
+ "name" : "Bad Empfing"
+ },
+ {
+ "ds100" : "TIMN",
+ "eva" : 8070309,
+ "latlong" : [
+ 48.404155,
+ 8.770092
+ ],
+ "name" : "Bad Imnau"
+ },
+ {
+ "ds100" : "LBSS",
+ "eva" : 8011083,
+ "latlong" : [
+ 51.6698138,
+ 12.7258694
+ ],
+ "name" : "Bad Schmiedeberg Süd"
+ },
+ {
+ "ds100" : "UBSN",
+ "eva" : 8011086,
+ "latlong" : [
+ 51.097428,
+ 11.6283
+ ],
+ "name" : "Bad Sulza Nord"
+ },
+ {
+ "ds100" : "TBW",
+ "eva" : 8000769,
+ "latlong" : [
+ 47.910258,
+ 9.889739
+ ],
+ "name" : "Bad Wurzach"
+ },
+ {
+ "ds100" : "ZWBA",
+ "eva" : 2100013,
+ "latlong" : [
+ 53.142611,
+ 26.027078
+ ],
+ "name" : "Baranovichi Centralnye"
+ },
+ {
+ "ds100" : "ABCL",
+ "eva" : 8071336,
+ "latlong" : [
+ 53.471851,
+ 9.027667
+ ],
+ "name" : "Barchel"
+ },
+ {
+ "ds100" : "LBAS",
+ "eva" : 8011112,
+ "latlong" : [
+ 52.214328,
+ 11.640852
+ ],
+ "name" : "Barleber See"
+ },
+ {
+ "ds100" : "XIBT",
+ "eva" : 8300397,
+ "latlong" : [
+ 40.605894,
+ 14.983259
+ ],
+ "name" : "Battipaglia"
+ },
+ {
+ "ds100" : "XSBLS",
+ "eva" : 8506159,
+ "latlong" : [
+ 47.6753379822752,
+ 9.0176698078502
+ ],
+ "name" : "Berlingen URh"
+ },
+ {
+ "ds100" : "XLXBB",
+ "eva" : 8271141,
+ "latlong" : [
+ 49.472074,
+ 6.107859
+ ],
+ "name" : "Bettembourg(fr)"
+ },
+ {
+ "ds100" : "XIBVA",
+ "eva" : 8301199,
+ "latlong" : [
+ 43.825194,
+ 7.579073
+ ],
+ "name" : "Bevera"
+ },
+ {
+ "ds100" : "RBIC",
+ "eva" : 8077777,
+ "latlong" : [
+ 47.967528,
+ 9.101812
+ ],
+ "name" : "Bichtlingen"
+ },
+ {
+ "ds100" : "RBZN",
+ "eva" : 8070323,
+ "latlong" : [
+ 47.631409,
+ 7.621048
+ ],
+ "name" : "Binzen"
+ },
+ {
+ "ds100" : "BBFD",
+ "eva" : 8089186,
+ "latlong" : [
+ 52.3380121,
+ 13.4156182
+ ],
+ "name" : "Blankenfelde (S)"
+ },
+ {
+ "ds100" : "LBLG",
+ "eva" : 8010056,
+ "latlong" : [
+ 52.033806,
+ 11.457093
+ ],
+ "name" : "Blumenberg"
+ },
+ {
+ "ds100" : "XIBD",
+ "eva" : 8300136,
+ "latlong" : [
+ 43.778323,
+ 7.663572
+ ],
+ "name" : "Bordighera"
+ },
+ {
+ "ds100" : "XDBOP",
+ "eva" : 8600276,
+ "latlong" : [
+ 55.494894,
+ 11.972927
+ ],
+ "name" : "Borup st"
+ },
+ {
+ "ds100" : "XFBZV",
+ "eva" : 8700333,
+ "latlong" : [
+ 49.289733,
+ 6.529723
+ ],
+ "name" : "Bouzonville"
+ },
+ {
+ "ds100" : "XTXBE",
+ "eva" : 5403739,
+ "latlong" : [
+ 48.712551,
+ 16.868402
+ ],
+ "name" : "Breclav(Gr)"
+ },
+ {
+ "ds100" : "KBK",
+ "eva" : 8071651,
+ "latlong" : [
+ 50.432442,
+ 7.168888
+ ],
+ "name" : "Brenk"
+ },
+ {
+ "ds100" : "ZWB",
+ "eva" : 2100001,
+ "latlong" : [
+ 52.100432,
+ 23.680681
+ ],
+ "name" : "Brest Central"
+ },
+ {
+ "ds100" : "XPXTE",
+ "eva" : 2100149,
+ "latlong" : [
+ 52.098902,
+ 23.673854
+ ],
+ "name" : "Brest(Gr)"
+ },
+ {
+ "ds100" : "XTBDN",
+ "eva" : 5438015,
+ "latlong" : [
+ 49.182897,
+ 16.615733
+ ],
+ "name" : "Brno dolni nadrazi"
+ },
+ {
+ "ds100" : "XTBRT",
+ "eva" : 5400612,
+ "latlong" : [
+ 50.951596,
+ 14.438788
+ ],
+ "name" : "Brtniky"
+ },
+ {
+ "ds100" : "XABU",
+ "eva" : 8100697,
+ "latlong" : [
+ 46.894811,
+ 15.834249
+ ],
+ "name" : "Burgfried b.Gnas"
+ },
+ {
+ "ds100" : "XEBU",
+ "eva" : 7100005,
+ "latlong" : [
+ 42.371166,
+ -3.666028
+ ],
+ "name" : "Burgos Rosa de Lima"
+ },
+ {
+ "ds100" : "TBUW",
+ "eva" : 8001281,
+ "latlong" : [
+ 47.922765,
+ 9.343805
+ ],
+ "name" : "Burgweiler"
+ },
+ {
+ "ds100" : "NBOB",
+ "eva" : 8070667,
+ "latlong" : [
+ 49.052994,
+ 13.014504
+ ],
+ "name" : "Böbrach"
+ },
+ {
+ "ds100" : "XECM",
+ "eva" : 7100076,
+ "latlong" : [
+ 41.841178,
+ 2.800607
+ ],
+ "name" : "Caldes de Malavella"
+ },
+ {
+ "ds100" : "XICTL",
+ "eva" : 8300375,
+ "latlong" : [
+ 44.496372,
+ 7.591296
+ ],
+ "name" : "Centallo"
+ },
+ {
+ "ds100" : "XTCC",
+ "eva" : 5400730,
+ "latlong" : [
+ 50.454323,
+ 13.363351
+ ],
+ "name" : "Cernovice u Chomutova"
+ },
+ {
+ "ds100" : "XTTR",
+ "eva" : 5400002,
+ "latlong" : [
+ 49.897324,
+ 16.446808
+ ],
+ "name" : "Ceska Trebova"
+ },
+ {
+ "ds100" : "DC B",
+ "eva" : 8071816,
+ "latlong" : [
+ 50.833196,
+ 12.925169
+ ],
+ "name" : "Chemnitz Stefan-Heym-Platz"
+ },
+ {
+ "ds100" : "ONCH",
+ "eva" : 8400152,
+ "latlong" : [
+ 50.876110076904,
+ 6.213844
+ ],
+ "name" : "Chevremont(NL)"
+ },
+ {
+ "ds100" : "XICU",
+ "eva" : 8300148,
+ "latlong" : [
+ 43.002469,
+ 11.957897
+ ],
+ "name" : "Chiusi-Chianciano Terme"
+ },
+ {
+ "ds100" : "ZUC",
+ "eva" : 2200010,
+ "latlong" : [
+ 48.432709,
+ 22.2055488
+ ],
+ "name" : "Chop"
+ },
+ {
+ "ds100" : "DCOL",
+ "eva" : 8011317,
+ "latlong" : [
+ 51.134287,
+ 12.799053
+ ],
+ "name" : "Colditz"
+ },
+ {
+ "ds100" : "XUCU",
+ "eva" : 5300001,
+ "latlong" : [
+ 46.341013,
+ 21.2977
+ ],
+ "name" : "Curtici"
+ },
+ {
+ "ds100" : "XPCD",
+ "eva" : 5100251,
+ "latlong" : [
+ 49.915103,
+ 19.00465
+ ],
+ "name" : "Czechowice-Dziedzice"
+ },
+ {
+ "ds100" : "SDAS",
+ "eva" : 8079081,
+ "latlong" : [
+ 49.148557,
+ 7.777208
+ ],
+ "name" : "Dahn Süd"
+ },
+ {
+ "ds100" : "ADS",
+ "eva" : 8070349,
+ "latlong" : [
+ 53.5319,
+ 9.442568
+ ],
+ "name" : "Deinste"
+ },
+ {
+ "ds100" : "XUD",
+ "eva" : 5300073,
+ "latlong" : [
+ 45.8842727,
+ 22.9102492
+ ],
+ "name" : "Deva"
+ },
+ {
+ "ds100" : "XIDM",
+ "eva" : 8300149,
+ "latlong" : [
+ 43.909659,
+ 8.078514
+ ],
+ "name" : "Diano Marina"
+ },
+ {
+ "ds100" : "XSDHS",
+ "eva" : 8506152,
+ "latlong" : [
+ 47.6904192083997,
+ 8.74885906156379
+ ],
+ "name" : "Diessenhofen URh"
+ },
+ {
+ "ds100" : "XZXDO",
+ "eva" : 7900042,
+ "latlong" : [
+ 45.8906308,
+ 15.6802375
+ ],
+ "name" : "Dobova(Gr)"
+ },
+ {
+ "ds100" : "XTDKR",
+ "eva" : 5400872,
+ "latlong" : [
+ 50.956423,
+ 14.518703
+ ],
+ "name" : "Dolni Krecany"
+ },
+ {
+ "ds100" : "XTDI",
+ "eva" : 5400905,
+ "latlong" : [
+ 50.479439,
+ 13.351523
+ ],
+ "name" : "Domina"
+ },
+ {
+ "ds100" : "TDOD",
+ "eva" : 8029358,
+ "latlong" : [
+ 48.228455,
+ 8.781119
+ ],
+ "name" : "Dotternhausen-Dormettingen"
+ },
+ {
+ "ds100" : "XLDF",
+ "eva" : 8270360,
+ "latlong" : [
+ 50.014874,
+ 6.006621
+ ],
+ "name" : "Drauffelt"
+ },
+ {
+ "ds100" : "EDBI",
+ "eva" : 8001599,
+ "latlong" : [
+ 51.392175,
+ 6.808067
+ ],
+ "name" : "Duisburg-Bissingheim"
+ },
+ {
+ "ds100" : "HDUS",
+ "eva" : 8070358,
+ "latlong" : [
+ 52.924916,
+ 8.627573
+ ],
+ "name" : "Dünsen DHE"
+ },
+ {
+ "ds100" : "NDW",
+ "eva" : 8070805,
+ "latlong" : [
+ 50.357182,
+ 11.516934
+ ],
+ "name" : "Dürrenwaid Bahnhof"
+ },
+ {
+ "ds100" : "XKEI",
+ "eva" : 7004419,
+ "latlong" : [
+ 51.442894,
+ 0.320865
+ ],
+ "name" : "Ebbsfleet International Eurostar"
+ },
+ {
+ "ds100" : "NEHM",
+ "eva" : 8070856,
+ "latlong" : [
+ 49.869412,
+ 10.153426
+ ],
+ "name" : "Eisenheim"
+ },
+ {
+ "ds100" : "TEND",
+ "eva" : 8029356,
+ "latlong" : [
+ 48.257384,
+ 8.83688
+ ],
+ "name" : "Endingen(Württ)"
+ },
+ {
+ "ds100" : "KENG",
+ "eva" : 8070368,
+ "latlong" : [
+ 50.424945,
+ 7.156853
+ ],
+ "name" : "Engeln"
+ },
+ {
+ "ds100" : "XSEMS",
+ "eva" : 8506162,
+ "latlong" : [
+ 47.6752332637346,
+ 9.08544469808097
+ ],
+ "name" : "Ermatingen URh"
+ },
+ {
+ "ds100" : "TERZ",
+ "eva" : 8029357,
+ "latlong" : [
+ 48.255409,
+ 8.815709
+ ],
+ "name" : "Erzingen(Württ)"
+ },
+ {
+ "ds100" : "NESD",
+ "eva" : 8070857,
+ "latlong" : [
+ 49.86674,
+ 10.165791
+ ],
+ "name" : "Escherndorf-Vogelsburg"
+ },
+ {
+ "ds100" : "AESL",
+ "eva" : 8071334,
+ "latlong" : [
+ 53.487691,
+ 9.272382
+ ],
+ "name" : "Essel"
+ },
+ {
+ "ds100" : "LEU",
+ "eva" : 8011522,
+ "latlong" : [
+ 51.827534,
+ 12.638466
+ ],
+ "name" : "Eutzsch"
+ },
+ {
+ "ds100" : "WFEL",
+ "eva" : 8011534,
+ "latlong" : [
+ 53.330754,
+ 13.431271
+ ],
+ "name" : "Feldberg(Meckl)"
+ },
+ {
+ "ds100" : "XIFE",
+ "eva" : 8300209,
+ "latlong" : [
+ 44.843119,
+ 11.603837
+ ],
+ "name" : "Ferrara"
+ },
+ {
+ "ds100" : "MFWG",
+ "eva" : 8070601,
+ "latlong" : [
+ 49.168721,
+ 10.324017
+ ],
+ "name" : "Feuchtwangen Bf"
+ },
+ {
+ "ds100" : "XEFI",
+ "eva" : 7100078,
+ "latlong" : [
+ 42.264952,
+ 2.969276
+ ],
+ "name" : "Figueres"
+ },
+ {
+ "ds100" : "XIFLM",
+ "eva" : 8300144,
+ "latlong" : [
+ 44.169247,
+ 8.340404
+ ],
+ "name" : "Finale Ligure Marina"
+ },
+ {
+ "ds100" : "NFD",
+ "eva" : 8002001,
+ "latlong" : [
+ 50.519551,
+ 10.150691
+ ],
+ "name" : "Fladungen"
+ },
+ {
+ "ds100" : "XEFL",
+ "eva" : 7100130,
+ "latlong" : [
+ 42.047541,
+ 2.957239
+ ],
+ "name" : "Flassa"
+ },
+ {
+ "ds100" : "XIF",
+ "eva" : 8300020,
+ "latlong" : [
+ 44.550582,
+ 7.717916
+ ],
+ "name" : "Fossano"
+ },
+ {
+ "ds100" : "AFRD",
+ "eva" : 8002113,
+ "latlong" : [
+ 53.528122,
+ 10.339671
+ ],
+ "name" : "Friedrichsruh"
+ },
+ {
+ "ds100" : "LFIO",
+ "eva" : 8011587,
+ "latlong" : [
+ 51.59782,
+ 11.323432
+ ],
+ "name" : "Friesdorf Ost"
+ },
+ {
+ "ds100" : "WGAL",
+ "eva" : 8011593,
+ "latlong" : [
+ 53.516641,
+ 12.129379
+ ],
+ "name" : "Gallin"
+ },
+ {
+ "ds100" : "UGOT",
+ "eva" : 8011617,
+ "latlong" : [
+ 50.849203,
+ 12.092791
+ ],
+ "name" : "Gera Ost"
+ },
+ {
+ "ds100" : "UGLW",
+ "eva" : 8011620,
+ "latlong" : [
+ 50.838429,
+ 12.080791
+ ],
+ "name" : "Gera-Liebschwitz"
+ },
+ {
+ "ds100" : "TGSN",
+ "eva" : 8007075,
+ "latlong" : [
+ 48.624836,
+ 10.022662
+ ],
+ "name" : "Gerstetten"
+ },
+ {
+ "ds100" : "AGNBN",
+ "eva" : 8070036,
+ "latlong" : [
+ 53.393287,
+ 9.015403
+ ],
+ "name" : "Gnarrenburg Nord"
+ },
+ {
+ "ds100" : "XPGW",
+ "eva" : 5100014,
+ "latlong" : [
+ 52.727753,
+ 15.229226
+ ],
+ "name" : "Gorzow Wlkp."
+ },
+ {
+ "ds100" : "XSGL",
+ "eva" : 8506163,
+ "latlong" : [
+ 47.6646656888434,
+ 9.13467870405456
+ ],
+ "name" : "Gottlieben (Schifflände)"
+ },
+ {
+ "ds100" : "XEGR",
+ "eva" : 7100075,
+ "latlong" : [
+ 41.599307,
+ 2.291623
+ ],
+ "name" : "Granollers"
+ },
+ {
+ "ds100" : "HGIP",
+ "eva" : 8070403,
+ "latlong" : [
+ 52.949135,
+ 8.632519
+ ],
+ "name" : "Groß Ippener DHE"
+ },
+ {
+ "ds100" : "LGRK",
+ "eva" : 8011685,
+ "latlong" : [
+ 51.602149,
+ 11.404245
+ ],
+ "name" : "Gräfenstuhl-Klippmühle"
+ },
+ {
+ "ds100" : "NSDT",
+ "eva" : 8070669,
+ "latlong" : [
+ 49.064357,
+ 12.944043
+ ],
+ "name" : "Gstadt(Wanderbahn)"
+ },
+ {
+ "ds100" : "TGSS",
+ "eva" : 8007074,
+ "latlong" : [
+ 48.639901,
+ 9.957623
+ ],
+ "name" : "Gussenstadt"
+ },
+ {
+ "ds100" : "FGLD",
+ "eva" : 8070257,
+ "latlong" : [
+ 49.599376,
+ 8.016444
+ ],
+ "name" : "Göllheim-Dreisen"
+ },
+ {
+ "ds100" : "HHAD",
+ "eva" : 8002497,
+ "latlong" : [
+ 52.711982,
+ 9.639434
+ ],
+ "name" : "Hademstorf"
+ },
+ {
+ "ds100" : "AHGN",
+ "eva" : 8071337,
+ "latlong" : [
+ 53.550361,
+ 9.460078
+ ],
+ "name" : "Hagen(Kr. Stade)"
+ },
+ {
+ "ds100" : "LHG",
+ "eva" : 8098159,
+ "latlong" : [
+ 51.478461,
+ 11.987868
+ ],
+ "name" : "Halle(Saale)Hbf Gl. 13a"
+ },
+ {
+ "ds100" : "MHMB",
+ "eva" : 966904,
+ "latlong" : [
+ 47.466024,
+ 11.046595
+ ],
+ "name" : "Hammersbach Zugspitzbahn, Grainau"
+ },
+ {
+ "ds100" : "FHAX",
+ "eva" : 8070253,
+ "latlong" : [
+ 49.64074,
+ 8.137885
+ ],
+ "name" : "Harxheim-Zell"
+ },
+ {
+ "ds100" : "XMXHY",
+ "eva" : 5501629,
+ "latlong" : [
+ 47.938258,
+ 17.095845
+ ],
+ "name" : "Hegyeshalom(Gr)"
+ },
+ {
+ "ds100" : "BHEL",
+ "eva" : 8011853,
+ "latlong" : [
+ 52.278758,
+ 14.478219
+ ],
+ "name" : "Helenesee"
+ },
+ {
+ "ds100" : "AHSN",
+ "eva" : 8002750,
+ "latlong" : [
+ 53.078795,
+ 9.811174
+ ],
+ "name" : "Hemsen(b Soltau)"
+ },
+ {
+ "ds100" : "RKMH",
+ "eva" : 721376,
+ "latlong" : [
+ 49.009815,
+ 8.400056
+ ],
+ "name" : "Herrenstraße, Karlsruhe"
+ },
+ {
+ "ds100" : "FKBHK",
+ "eva" : 713942,
+ "latlong" : [
+ 51.329768,
+ 9.521922
+ ],
+ "name" : "Hinter dem Fasanenhof, Kassel"
+ },
+ {
+ "ds100" : "SHW",
+ "eva" : 8079147,
+ "latlong" : [
+ 49.203132,
+ 7.773047
+ ],
+ "name" : "Hinterweidenthal Ost"
+ },
+ {
+ "ds100" : "BHOW",
+ "eva" : 8080710,
+ "latlong" : [
+ 52.672398,
+ 13.271342
+ ],
+ "name" : "Hohen Neuendorf West"
+ },
+ {
+ "ds100" : "XSHBS",
+ "eva" : 8506111,
+ "latlong" : [
+ 47.496639074807,
+ 9.46295013675514
+ ],
+ "name" : "Horn(Bodensee), SF"
+ },
+ {
+ "ds100" : "RHFH",
+ "eva" : 8007439,
+ "latlong" : [
+ 49.291132,
+ 9.086804
+ ],
+ "name" : "Hüffenhardt"
+ },
+ {
+ "ds100" : "AHTB",
+ "eva" : 8007119,
+ "latlong" : [
+ 53.273296,
+ 8.962154
+ ],
+ "name" : "Hüttenbusch"
+ },
+ {
+ "ds100" : "FIN",
+ "eva" : 8003077,
+ "latlong" : [
+ 50.460379,
+ 8.892407
+ ],
+ "name" : "Inheiden"
+ },
+ {
+ "ds100" : "XEIR",
+ "eva" : 7100009,
+ "latlong" : [
+ 43.339403,
+ -1.801153
+ ],
+ "name" : "Irun"
+ },
+ {
+ "ds100" : "XIXIT",
+ "eva" : 8300366,
+ "latlong" : [
+ 46.187965,
+ 8.187302
+ ],
+ "name" : "Iselle transito"
+ },
+ {
+ "ds100" : "XAXRB",
+ "eva" : 7900043,
+ "latlong" : [
+ 46.481412,
+ 14.020333
+ ],
+ "name" : "Jesenice(Gr)"
+ },
+ {
+ "ds100" : "MJGS",
+ "eva" : 8003106,
+ "latlong" : [
+ 47.666782,
+ 11.087702
+ ],
+ "name" : "Jägerhaus"
+ },
+ {
+ "ds100" : "MKAI",
+ "eva" : 8003146,
+ "latlong" : [
+ 47.482981,
+ 11.116723
+ ],
+ "name" : "Kainzenbad"
+ },
+ {
+ "ds100" : "WKGW",
+ "eva" : 8011989,
+ "latlong" : [
+ 53.49952404,
+ 12.76242345
+ ],
+ "name" : "Kargow"
+ },
+ {
+ "ds100" : "XMK",
+ "eva" : 5500092,
+ "latlong" : [
+ 46.194865,
+ 19.610156
+ ],
+ "name" : "Kelebia"
+ },
+ {
+ "ds100" : "WKLE",
+ "eva" : 8012020,
+ "latlong" : [
+ 53.62316794,
+ 13.06042745
+ ],
+ "name" : "Kleeth"
+ },
+ {
+ "ds100" : "DKFR",
+ "eva" : 8017344,
+ "latlong" : [
+ 51.289921,
+ 13.104999
+ ],
+ "name" : "Kleinforst Rosensee"
+ },
+ {
+ "ds100" : "WKC",
+ "eva" : 8012049,
+ "latlong" : [
+ 53.470795,
+ 12.86687
+ ],
+ "name" : "Klockow(b Waren/Müritz)"
+ },
+ {
+ "ds100" : "LKMR",
+ "eva" : 8010204,
+ "latlong" : [
+ 51.59109,
+ 11.498511
+ ],
+ "name" : "Klostermansfeld Randsiedlung"
+ },
+ {
+ "ds100" : "LKNF",
+ "eva" : 8012059,
+ "latlong" : [
+ 51.247132,
+ 12.272945
+ ],
+ "name" : "Knautnaundorf"
+ },
+ {
+ "ds100" : "XDKG",
+ "eva" : 8601312,
+ "latlong" : [
+ 55.4575,
+ 12.186472
+ ],
+ "name" : "Koege st"
+ },
+ {
+ "ds100" : "XDKS",
+ "eva" : 8601343,
+ "latlong" : [
+ 55.357201,
+ 11.135125
+ ],
+ "name" : "Korsoer st"
+ },
+ {
+ "ds100" : "XTKO",
+ "eva" : 5401571,
+ "latlong" : [
+ 50.430178,
+ 13.035015
+ ],
+ "name" : "Kovarska"
+ },
+ {
+ "ds100" : "KKRZ",
+ "eva" : 8003436,
+ "latlong" : [
+ 50.506442,
+ 6.978557
+ ],
+ "name" : "Kreuzberg(Ahr)"
+ },
+ {
+ "ds100" : "MKZB",
+ "eva" : 966903,
+ "latlong" : [
+ 47.472102,
+ 11.062936
+ ],
+ "name" : "Kreuzeck/Alpspitzbahn Bahnhof, Garmisch-Partenkirc"
+ },
+ {
+ "ds100" : "XTKM",
+ "eva" : 5401649,
+ "latlong" : [
+ 50.489755,
+ 13.276007
+ ],
+ "name" : "Krimov"
+ },
+ {
+ "ds100" : "KKDZB",
+ "eva" : 8083368,
+ "latlong" : [
+ 50.941299,
+ 6.974641
+ ],
+ "name" : "Köln Messe/Deutz Gl. 9-10"
+ },
+ {
+ "ds100" : "MLQU",
+ "eva" : 8070812,
+ "latlong" : [
+ 48.821058,
+ 12.053477
+ ],
+ "name" : "Langquaid(b Eggmühl)"
+ },
+ {
+ "ds100" : "LLSG",
+ "eva" : 8012173,
+ "latlong" : [
+ 51.540742,
+ 12.639613
+ ],
+ "name" : "Laußig(Düben)"
+ },
+ {
+ "ds100" : "XTLM",
+ "eva" : 5401791,
+ "latlong" : [
+ 50.532482,
+ 14.138833
+ ],
+ "name" : "Litomerice mesto"
+ },
+ {
+ "ds100" : "XILI",
+ "eva" : 8300157,
+ "latlong" : [
+ 43.553983,
+ 10.336626
+ ],
+ "name" : "Livorno Centrale"
+ },
+ {
+ "ds100" : "XSMMS",
+ "eva" : 8506155,
+ "latlong" : [
+ 47.6482873603292,
+ 8.91673503093438
+ ],
+ "name" : "Mammern URh"
+ },
+ {
+ "ds100" : "XSMBS",
+ "eva" : 8506160,
+ "latlong" : [
+ 47.6751705431517,
+ 9.04756474024539
+ ],
+ "name" : "Mannenbach URh"
+ },
+ {
+ "ds100" : "LMAS",
+ "eva" : 8012300,
+ "latlong" : [
+ 51.600325,
+ 11.452086
+ ],
+ "name" : "Mansfeld(Südharz)"
+ },
+ {
+ "ds100" : "DMG",
+ "eva" : 8012301,
+ "latlong" : [
+ 50.648287,
+ 13.163227
+ ],
+ "name" : "Marienberg(Sachs)"
+ },
+ {
+ "ds100" : "PQKMP",
+ "eva" : 371861,
+ "latlong" : [
+ 49.009485,
+ 8.405563
+ ],
+ "name" : "Marktplatz, Karlsruhe"
+ },
+ {
+ "ds100" : "LMDF",
+ "eva" : 8012325,
+ "latlong" : [
+ 51.725714,
+ 11.294529
+ ],
+ "name" : "Meisdorf"
+ },
+ {
+ "ds100" : "RMEN",
+ "eva" : 8077779,
+ "latlong" : [
+ 48.0067397,
+ 9.1609604
+ ],
+ "name" : "Menningen-Leitishofen"
+ },
+ {
+ "ds100" : "TNHU",
+ "eva" : 8070678,
+ "latlong" : [
+ 48.532647,
+ 9.307106
+ ],
+ "name" : "Metzingen-Neuhausen"
+ },
+ {
+ "ds100" : "FMICH",
+ "eva" : 8070165,
+ "latlong" : [
+ 50.099783,
+ 9.118298
+ ],
+ "name" : "Michelbach(Unterfr) Herrnmühle"
+ },
+ {
+ "ds100" : "XDMF",
+ "eva" : 8601593,
+ "latlong" : [
+ 55.501623,
+ 9.734662
+ ],
+ "name" : "Middelfart st"
+ },
+ {
+ "ds100" : "XTMHN",
+ "eva" : 5401976,
+ "latlong" : [
+ 50.956468,
+ 14.390211
+ ],
+ "name" : "Mikulasovice horni nadrazi"
+ },
+ {
+ "ds100" : "XTMHS",
+ "eva" : 5401977,
+ "latlong" : [
+ 50.962356,
+ 14.357535
+ ],
+ "name" : "Mikulasovice stred"
+ },
+ {
+ "ds100" : "ZWM",
+ "eva" : 2100003,
+ "latlong" : [
+ 53.890736,
+ 27.55064
+ ],
+ "name" : "Minsk-Passajirskii"
+ },
+ {
+ "ds100" : "XSMOS",
+ "eva" : 8509415,
+ "latlong" : [
+ 47.112987492662,
+ 9.27680343664119
+ ],
+ "name" : "Mols"
+ },
+ {
+ "ds100" : "SMBT",
+ "eva" : 8079265,
+ "latlong" : [
+ 49.165954,
+ 7.754959
+ ],
+ "name" : "Moosbachtal"
+ },
+ {
+ "ds100" : "XCM",
+ "eva" : 2000058,
+ "latlong" : [
+ 55.776672,
+ 37.57981
+ ],
+ "name" : "Moskva Belorusskaja"
+ },
+ {
+ "ds100" : "DMUES",
+ "eva" : 8070696,
+ "latlong" : [
+ 51.238593,
+ 13.047845
+ ],
+ "name" : "Mügeln Stadt"
+ },
+ {
+ "ds100" : "TMRN",
+ "eva" : 8070494,
+ "latlong" : [
+ 48.420328,
+ 8.760169
+ ],
+ "name" : "Mühringen"
+ },
+ {
+ "ds100" : "MS",
+ "eva" : 8099501,
+ "latlong" : [
+ 48.12309,
+ 11.551565
+ ],
+ "name" : "München-Süd"
+ },
+ {
+ "ds100" : "XDNV",
+ "eva" : 8601645,
+ "latlong" : [
+ 55.231875,
+ 11.766926
+ ],
+ "name" : "Naestved st"
+ },
+ {
+ "ds100" : "DNAO",
+ "eva" : 8070691,
+ "latlong" : [
+ 51.25978,
+ 13.101472
+ ],
+ "name" : "Naundorf (b Oschatz)"
+ },
+ {
+ "ds100" : "RNHS",
+ "eva" : 8007434,
+ "latlong" : [
+ 49.296035,
+ 8.963686
+ ],
+ "name" : "Neckarbischofsheim Stadt"
+ },
+ {
+ "ds100" : "D730823",
+ "eva" : 730823,
+ "latlong" : [
+ 52.544431,
+ 13.370133
+ ],
+ "name" : "Nettelbeckplatz/Wedding (S), Berlin"
+ },
+ {
+ "ds100" : "HNPL",
+ "eva" : 8004260,
+ "latlong" : [
+ 52.548702,
+ 10.60754
+ ],
+ "name" : "Neudorf-Platendorf"
+ },
+ {
+ "ds100" : "MNHL",
+ "eva" : 8026358,
+ "latlong" : [
+ 48.618764,
+ 11.996039
+ ],
+ "name" : "Neuhausen(b Landshut)"
+ },
+ {
+ "ds100" : "WNRS",
+ "eva" : 8080175,
+ "latlong" : [
+ 52.92535,
+ 12.82751
+ ],
+ "name" : "Neuruppin Seedamm"
+ },
+ {
+ "ds100" : "XDNP",
+ "eva" : 8601699,
+ "latlong" : [
+ 55.683522,
+ 12.571867
+ ],
+ "name" : "Noerreport st"
+ },
+ {
+ "ds100" : "ANDS",
+ "eva" : 8007116,
+ "latlong" : [
+ 53.332837,
+ 8.980288
+ ],
+ "name" : "Nordsode"
+ },
+ {
+ "ds100" : "XJNB",
+ "eva" : 7200210,
+ "latlong" : [
+ 44.806916,
+ 20.418098
+ ],
+ "name" : "Novi Beograd"
+ },
+ {
+ "ds100" : "XJNS",
+ "eva" : 7200008,
+ "latlong" : [
+ 45.2657352,
+ 19.8292601
+ ],
+ "name" : "Novi Sad"
+ },
+ {
+ "ds100" : "NNBS",
+ "eva" : 8070668,
+ "latlong" : [
+ 49.070682,
+ 12.965009
+ ],
+ "name" : "Nußberg-Schönau"
+ },
+ {
+ "ds100" : "XDNE",
+ "eva" : 8613687,
+ "latlong" : [
+ 55.652421,
+ 12.516317
+ ],
+ "name" : "Ny Ellebjerg st"
+ },
+ {
+ "ds100" : "XDNY",
+ "eva" : 8601739,
+ "latlong" : [
+ 55.314068,
+ 10.802921
+ ],
+ "name" : "Nyborg st"
+ },
+ {
+ "ds100" : "XDNK",
+ "eva" : 8601745,
+ "latlong" : [
+ 54.766833,
+ 11.877767
+ ],
+ "name" : "Nykoebing F st"
+ },
+ {
+ "ds100" : "NND",
+ "eva" : 8098493,
+ "latlong" : [
+ 49.431761,
+ 11.127479
+ ],
+ "name" : "Nürnberg Frankenstadion Sonderbahnsteig"
+ },
+ {
+ "ds100" : "ROGI",
+ "eva" : 8007437,
+ "latlong" : [
+ 49.255404,
+ 9.044019
+ ],
+ "name" : "Obergimpern"
+ },
+ {
+ "ds100" : "UOF",
+ "eva" : 8012525,
+ "latlong" : [
+ 50.684213,
+ 10.709405
+ ],
+ "name" : "Oberhof(Thür)"
+ },
+ {
+ "ds100" : "MOBG",
+ "eva" : 8070803,
+ "latlong" : [
+ 47.998779,
+ 12.403156
+ ],
+ "name" : "Obing"
+ },
+ {
+ "ds100" : "XDKHO",
+ "eva" : 8601560,
+ "latlong" : [
+ 55.6290128,
+ 12.579355
+ ],
+ "name" : "Oerestad st"
+ },
+ {
+ "ds100" : "EOES",
+ "eva" : 8004631,
+ "latlong" : [
+ 51.402051,
+ 7.788853
+ ],
+ "name" : "Oese"
+ },
+ {
+ "ds100" : "XDOP",
+ "eva" : 8601878,
+ "latlong" : [
+ 55.692708,
+ 12.587615
+ ],
+ "name" : "Oesterport st"
+ },
+ {
+ "ds100" : "XROK",
+ "eva" : 7800036,
+ "latlong" : [
+ 45.2524891,
+ 17.2063555
+ ],
+ "name" : "Okucani"
+ },
+ {
+ "ds100" : "ZWO",
+ "eva" : 2100012,
+ "latlong" : [
+ 54.520839,
+ 30.374882
+ ],
+ "name" : "Orscha Central"
+ },
+ {
+ "ds100" : "XIOV",
+ "eva" : 8300257,
+ "latlong" : [
+ 42.72393,
+ 12.126752
+ ],
+ "name" : "Orvieto"
+ },
+ {
+ "ds100" : "DOT K",
+ "eva" : 8070686,
+ "latlong" : [
+ 51.300102,
+ 13.111648
+ ],
+ "name" : "Oschatz Körnerstr"
+ },
+ {
+ "ds100" : "DOT L",
+ "eva" : 8070685,
+ "latlong" : [
+ 51.302517,
+ 13.109548
+ ],
+ "name" : "Oschatz Lichtstr"
+ },
+ {
+ "ds100" : "DOTS",
+ "eva" : 8070688,
+ "latlong" : [
+ 51.294173,
+ 13.110174
+ ],
+ "name" : "Oschatz Südbf"
+ },
+ {
+ "ds100" : "NOMR",
+ "eva" : 8004711,
+ "latlong" : [
+ 50.455926,
+ 10.231621
+ ],
+ "name" : "Ostheim v Rhön"
+ },
+ {
+ "ds100" : "XPOM",
+ "eva" : 5100048,
+ "latlong" : [
+ 50.041503,
+ 19.199724
+ ],
+ "name" : "Oswiecim"
+ },
+ {
+ "ds100" : "XTPAN",
+ "eva" : 5402315,
+ "latlong" : [
+ 50.948252,
+ 14.463796
+ ],
+ "name" : "Pansky"
+ },
+ {
+ "ds100" : "WPAW",
+ "eva" : 8012612,
+ "latlong" : [
+ 53.50811,
+ 12.054112
+ ],
+ "name" : "Passow(Meckl)"
+ },
+ {
+ "ds100" : "MPFT",
+ "eva" : 8026355,
+ "latlong" : [
+ 48.574499,
+ 12.066113
+ ],
+ "name" : "Pfettrach"
+ },
+ {
+ "ds100" : "XSXPT",
+ "eva" : 8505409,
+ "latlong" : [
+ 46.1045508232004,
+ 8.7570199823613
+ ],
+ "name" : "Pino transito"
+ },
+ {
+ "ds100" : "XIPI",
+ "eva" : 8300169,
+ "latlong" : [
+ 43.707973,
+ 10.398174
+ ],
+ "name" : "Pisa Centrale"
+ },
+ {
+ "ds100" : "MPTH",
+ "eva" : 8072764,
+ "latlong" : [
+ 47.986762,
+ 12.376341
+ ],
+ "name" : "Pittenhart"
+ },
+ {
+ "ds100" : "DPU",
+ "eva" : 8013463,
+ "latlong" : [
+ 50.48624,
+ 12.129382
+ ],
+ "name" : "Plauen(V) unt Bf"
+ },
+ {
+ "ds100" : "WPRH",
+ "eva" : 8074703,
+ "latlong" : [
+ 53.170082,
+ 12.20592
+ ],
+ "name" : "Pritzwalk Hainholz"
+ },
+ {
+ "ds100" : "APSH",
+ "eva" : 8007318,
+ "latlong" : [
+ 54.365139,
+ 10.292991
+ ],
+ "name" : "Probsteierhagen"
+ },
+ {
+ "ds100" : "NPM",
+ "eva" : 8070858,
+ "latlong" : [
+ 49.864518,
+ 10.123567
+ ],
+ "name" : "Prosselsheim"
+ },
+ {
+ "ds100" : "ERAE",
+ "eva" : 8004918,
+ "latlong" : [
+ 51.965867,
+ 7.867547
+ ],
+ "name" : "Raestrup-Everswinkel"
+ },
+ {
+ "ds100" : "KREC",
+ "eva" : 8004967,
+ "latlong" : [
+ 50.515427,
+ 7.036514
+ ],
+ "name" : "Rech"
+ },
+ {
+ "ds100" : "MRFR",
+ "eva" : 966907,
+ "latlong" : [
+ 47.43554,
+ 10.987337
+ ],
+ "name" : "Riffelriß, Grainau"
+ },
+ {
+ "ds100" : "XIRI",
+ "eva" : 8300221,
+ "latlong" : [
+ 44.064237,
+ 12.574016
+ ],
+ "name" : "Rimini"
+ },
+ {
+ "ds100" : "XDR",
+ "eva" : 8601988,
+ "latlong" : [
+ 54.655717,
+ 11.357463
+ ],
+ "name" : "Roedby"
+ },
+ {
+ "ds100" : "XSRHF",
+ "eva" : 8506112,
+ "latlong" : [
+ 47.5655617030878,
+ 9.37997531046235
+ ],
+ "name" : "Romanshorn (See)"
+ },
+ {
+ "ds100" : "XSRSS",
+ "eva" : 8506113,
+ "latlong" : [
+ 47.4789070232403,
+ 9.49250636652451
+ ],
+ "name" : "Rorschach Hafen (See)"
+ },
+ {
+ "ds100" : "MROS",
+ "eva" : 8005173,
+ "latlong" : [
+ 47.866304,
+ 12.104178
+ ],
+ "name" : "Rosenheim Hochschule"
+ },
+ {
+ "ds100" : "XDRK",
+ "eva" : 8602026,
+ "latlong" : [
+ 55.639014,
+ 12.088854
+ ],
+ "name" : "Roskilde st"
+ },
+ {
+ "ds100" : "XNRS",
+ "eva" : 8400015,
+ "latlong" : [
+ 51.893890380859,
+ 4.5197219848633
+ ],
+ "name" : "Rotterdam Stadion"
+ },
+ {
+ "ds100" : "XIRG",
+ "eva" : 8300238,
+ "latlong" : [
+ 45.076985,
+ 11.781064
+ ],
+ "name" : "Rovigo"
+ },
+ {
+ "ds100" : "TROS",
+ "eva" : 8005174,
+ "latlong" : [
+ 47.86541789,
+ 9.78662425
+ ],
+ "name" : "Roßberg"
+ },
+ {
+ "ds100" : "XJRU",
+ "eva" : 7200010,
+ "latlong" : [
+ 44.9895547,
+ 19.8270339
+ ],
+ "name" : "Ruma"
+ },
+ {
+ "ds100" : "XTRS",
+ "eva" : 5402723,
+ "latlong" : [
+ 50.450027,
+ 13.169033
+ ],
+ "name" : "Rusova"
+ },
+ {
+ "ds100" : "XFRF",
+ "eva" : 8701876,
+ "latlong" : [
+ 49.091525,
+ 7.091677
+ ],
+ "name" : "Rémelfing"
+ },
+ {
+ "ds100" : "RRMM",
+ "eva" : 8070524,
+ "latlong" : [
+ 47.642926,
+ 7.640709
+ ],
+ "name" : "Rümmingen"
+ },
+ {
+ "ds100" : "XISAR",
+ "eva" : 8300182,
+ "latlong" : [
+ 43.829038,
+ 7.784346
+ ],
+ "name" : "San Remo"
+ },
+ {
+ "ds100" : "HSAB",
+ "eva" : 8005285,
+ "latlong" : [
+ 53.507247,
+ 8.014777
+ ],
+ "name" : "Sanderbusch"
+ },
+ {
+ "ds100" : "XRXSJ",
+ "eva" : 7800076,
+ "latlong" : [
+ 45.505158,
+ 14.244213
+ ],
+ "name" : "Sapjane(Gr)"
+ },
+ {
+ "ds100" : "XNSO",
+ "eva" : 8400545,
+ "latlong" : [
+ 53.1589183,
+ 6.7955648
+ ],
+ "name" : "Sappemeer Oost"
+ },
+ {
+ "ds100" : "XFSI",
+ "eva" : 8700528,
+ "latlong" : [
+ 49.086193,
+ 7.111315
+ ],
+ "name" : "Sarreinsming"
+ },
+ {
+ "ds100" : "RSDO",
+ "eva" : 8077776,
+ "latlong" : [
+ 47.944482,
+ 9.0946518
+ ],
+ "name" : "Sauldorf"
+ },
+ {
+ "ds100" : "XISAV",
+ "eva" : 8300185,
+ "latlong" : [
+ 44.307276,
+ 8.470105
+ ],
+ "name" : "Savona"
+ },
+ {
+ "ds100" : "TSKS",
+ "eva" : 8007072,
+ "latlong" : [
+ 48.611798,
+ 9.91133
+ ],
+ "name" : "Schalkstetten"
+ },
+ {
+ "ds100" : "MSC",
+ "eva" : 8005327,
+ "latlong" : [
+ 47.927051,
+ 12.126724
+ ],
+ "name" : "Schechen"
+ },
+ {
+ "ds100" : "MSLG",
+ "eva" : 8070811,
+ "latlong" : [
+ 48.831851,
+ 12.142663
+ ],
+ "name" : "Schierling"
+ },
+ {
+ "ds100" : "DSWO",
+ "eva" : 8070692,
+ "latlong" : [
+ 51.245226,
+ 13.084558
+ ],
+ "name" : "Schweta Gasth"
+ },
+ {
+ "ds100" : "TSCS",
+ "eva" : 8072211,
+ "latlong" : [
+ 48.21129,
+ 8.771335
+ ],
+ "name" : "Schömberg Stausee"
+ },
+ {
+ "ds100" : "TSCB",
+ "eva" : 8029359,
+ "latlong" : [
+ 48.205743,
+ 8.758503
+ ],
+ "name" : "Schömberg(b Balingen)"
+ },
+ {
+ "ds100" : "ASCK",
+ "eva" : 8007314,
+ "latlong" : [
+ 54.332873,
+ 10.229329
+ ],
+ "name" : "Schönkirchen Bf"
+ },
+ {
+ "ds100" : "XTSBZ",
+ "eva" : 5402772,
+ "latlong" : [
+ 50.593048,
+ 14.053958
+ ],
+ "name" : "Sebuzin"
+ },
+ {
+ "ds100" : "XJSI",
+ "eva" : 7200140,
+ "latlong" : [
+ 45.117524,
+ 19.221557
+ ],
+ "name" : "Sid(SRB)"
+ },
+ {
+ "ds100" : "RSGB",
+ "eva" : 8007438,
+ "latlong" : [
+ 49.271227,
+ 9.087386
+ ],
+ "name" : "Siegelsbach"
+ },
+ {
+ "ds100" : "LSIE",
+ "eva" : 8012992,
+ "latlong" : [
+ 52.818929,
+ 12.397722
+ ],
+ "name" : "Sieversdorf(Neust/D)"
+ },
+ {
+ "ds100" : "XUSI",
+ "eva" : 5300003,
+ "latlong" : [
+ 45.846272,
+ 23.013784
+ ],
+ "name" : "Simeria"
+ },
+ {
+ "ds100" : "XZSL",
+ "eva" : 7900048,
+ "latlong" : [
+ 46.1743369,
+ 14.3356158
+ ],
+ "name" : "Skofja Loka"
+ },
+ {
+ "ds100" : "XDSGE",
+ "eva" : 8602254,
+ "latlong" : [
+ 55.407553,
+ 11.348057
+ ],
+ "name" : "Slagelse st"
+ },
+ {
+ "ds100" : "XCSM",
+ "eva" : 2000004,
+ "latlong" : [
+ 54.798343,
+ 32.034466
+ ],
+ "name" : "Smolensk"
+ },
+ {
+ "ds100" : "XDSOR",
+ "eva" : 8602350,
+ "latlong" : [
+ 55.419019,
+ 11.568794
+ ],
+ "name" : "Soroe st"
+ },
+ {
+ "ds100" : "XJSM",
+ "eva" : 7200011,
+ "latlong" : [
+ 44.9823572,
+ 19.6136963
+ ],
+ "name" : "Sremska Mitrovica"
+ },
+ {
+ "ds100" : "XFSHT",
+ "eva" : 8701972,
+ "latlong" : [
+ 49.055928,
+ 4.379409
+ ],
+ "name" : "St-Hilaire-au-Temple"
+ },
+ {
+ "ds100" : "XJSP",
+ "eva" : 7200174,
+ "latlong" : [
+ 44.98629,
+ 20.138006
+ ],
+ "name" : "Stara Pazova"
+ },
+ {
+ "ds100" : "XTSKR",
+ "eva" : 5402921,
+ "latlong" : [
+ 50.947794,
+ 14.491942
+ ],
+ "name" : "Stare Krecany"
+ },
+ {
+ "ds100" : "XSSBS",
+ "eva" : 8506157,
+ "latlong" : [
+ 47.6686469246848,
+ 8.98143540966547
+ ],
+ "name" : "Steckborn URh"
+ },
+ {
+ "ds100" : "NSTW",
+ "eva" : 8005716,
+ "latlong" : [
+ 50.292441,
+ 11.462246
+ ],
+ "name" : "Steinwiesen Bf"
+ },
+ {
+ "ds100" : "TSTT",
+ "eva" : 8070548,
+ "latlong" : [
+ 48.353415,
+ 8.812741
+ ],
+ "name" : "Stetten (b. Haigerloch)"
+ },
+ {
+ "ds100" : "KST G",
+ "eva" : 8099506,
+ "latlong" : [
+ 50.797555,
+ 6.22488
+ ],
+ "name" : "Stolberg(Rheinl)Gbf"
+ },
+ {
+ "ds100" : "TSBH",
+ "eva" : 8007071,
+ "latlong" : [
+ 48.593792,
+ 9.920377
+ ],
+ "name" : "Stubersheim"
+ },
+ {
+ "ds100" : "XJST",
+ "eva" : 7200012,
+ "latlong" : [
+ 46.102069,
+ 19.671131
+ ],
+ "name" : "Subotica"
+ },
+ {
+ "ds100" : "XMXSB",
+ "eva" : 5603743,
+ "latlong" : [
+ 47.824896,
+ 18.844774
+ ],
+ "name" : "Szob(Gr)"
+ },
+ {
+ "ds100" : "LSOL",
+ "eva" : 8013001,
+ "latlong" : [
+ 51.633456,
+ 12.654031
+ ],
+ "name" : "Söllichau"
+ },
+ {
+ "ds100" : "XITG",
+ "eva" : 8300176,
+ "latlong" : [
+ 43.844625,
+ 7.855336
+ ],
+ "name" : "Taggia"
+ },
+ {
+ "ds100" : "XDTA",
+ "eva" : 8602526,
+ "latlong" : [
+ 55.545016,
+ 9.61571
+ ],
+ "name" : "Taulov st"
+ },
+ {
+ "ds100" : "XPTE",
+ "eva" : 5100084,
+ "latlong" : [
+ 52.07414,
+ 23.601227
+ ],
+ "name" : "Terespol"
+ },
+ {
+ "ds100" : "NTFB",
+ "eva" : 8026544,
+ "latlong" : [
+ 48.61502,
+ 13.42308
+ ],
+ "name" : "Tiefenbach(b Passau)"
+ },
+ {
+ "ds100" : "XDTP",
+ "eva" : 8602588,
+ "latlong" : [
+ 55.349752,
+ 10.182602
+ ],
+ "name" : "Tommerup st"
+ },
+ {
+ "ds100" : "XRT",
+ "eva" : 7800197,
+ "latlong" : [
+ 45.155272,
+ 19.148794
+ ],
+ "name" : "Tovarnik"
+ },
+ {
+ "ds100" : "LTRB",
+ "eva" : 8013140,
+ "latlong" : [
+ 51.704991,
+ 11.763092
+ ],
+ "name" : "Trebitz(Könnern)"
+ },
+ {
+ "ds100" : "XVTF",
+ "eva" : 7400795,
+ "latlong" : [
+ 55.372579,
+ 13.151892
+ ],
+ "name" : "Trelleborg F"
+ },
+ {
+ "ds100" : "XATK",
+ "eva" : 8100379,
+ "latlong" : [
+ 48.027377,
+ 12.860483
+ ],
+ "name" : "Trimmelkam"
+ },
+ {
+ "ds100" : "UTRO",
+ "eva" : 8013152,
+ "latlong" : [
+ 51.118495,
+ 11.481268
+ ],
+ "name" : "Tromsdorf"
+ },
+ {
+ "ds100" : "RKTUT",
+ "eva" : 720995,
+ "latlong" : [
+ 49.006769,
+ 8.431264
+ ],
+ "name" : "Tullastraße/Verkehrsbetriebe, Karlsruhe"
+ },
+ {
+ "ds100" : "XPTY",
+ "eva" : 5100260,
+ "latlong" : [
+ 50.136194,
+ 18.964092
+ ],
+ "name" : "Tychy"
+ },
+ {
+ "ds100" : "MURB",
+ "eva" : 8005961,
+ "latlong" : [
+ 47.818772,
+ 12.320636
+ ],
+ "name" : "Umrathshausen Bf"
+ },
+ {
+ "ds100" : "XDVAL",
+ "eva" : 8602714,
+ "latlong" : [
+ 55.663794,
+ 12.514324
+ ],
+ "name" : "Valby(Koebenhavn)"
+ },
+ {
+ "ds100" : "LVT",
+ "eva" : 8013182,
+ "latlong" : [
+ 51.597687,
+ 11.428993
+ ],
+ "name" : "Vatterode"
+ },
+ {
+ "ds100" : "LVTT",
+ "eva" : 8079152,
+ "latlong" : [
+ 51.598218,
+ 11.412188
+ ],
+ "name" : "Vatteröder Teich"
+ },
+ {
+ "ds100" : "XTVY",
+ "eva" : 5400383,
+ "latlong" : [
+ 50.500192,
+ 13.033743
+ ],
+ "name" : "Vejprty"
+ },
+ {
+ "ds100" : "XTVZ",
+ "eva" : 5403344,
+ "latlong" : [
+ 50.526096,
+ 14.080554
+ ],
+ "name" : "Velke Zernoseky"
+ },
+ {
+ "ds100" : "XDVSJ",
+ "eva" : 8602825,
+ "latlong" : [
+ 55.544854,
+ 12.02755
+ ],
+ "name" : "Viby Sjaelland st"
+ },
+ {
+ "ds100" : "XFVT",
+ "eva" : 8700520,
+ "latlong" : [
+ 48.202506,
+ 5.941865
+ ],
+ "name" : "Vittel"
+ },
+ {
+ "ds100" : "XIVOG",
+ "eva" : 8300492,
+ "latlong" : [
+ 44.997908,
+ 9.008735
+ ],
+ "name" : "Voghera"
+ },
+ {
+ "ds100" : "NVOLA",
+ "eva" : 8070860,
+ "latlong" : [
+ 49.864345,
+ 10.217033
+ ],
+ "name" : "Volkach-Astheim"
+ },
+ {
+ "ds100" : "XDVB",
+ "eva" : 8602903,
+ "latlong" : [
+ 55.012519,
+ 11.899413
+ ],
+ "name" : "Vordingborg st"
+ },
+ {
+ "ds100" : "FWAM",
+ "eva" : 8006126,
+ "latlong" : [
+ 49.635362,
+ 8.168439
+ ],
+ "name" : "Wachenheim-Mölsheim"
+ },
+ {
+ "ds100" : "TWHS",
+ "eva" : 8007073,
+ "latlong" : [
+ 48.635308,
+ 9.907723
+ ],
+ "name" : "Waldhausen(b Geislingen)"
+ },
+ {
+ "ds100" : "OFWL",
+ "eva" : 8702265,
+ "latlong" : [
+ 49.22391,
+ 6.15963
+ ],
+ "name" : "Walygator Parc"
+ },
+ {
+ "ds100" : "RWZ",
+ "eva" : 8070569,
+ "latlong" : [
+ 47.768832,
+ 8.476514
+ ],
+ "name" : "Weizen"
+ },
+ {
+ "ds100" : "EWGO",
+ "eva" : 8070571,
+ "latlong" : [
+ 51.40007591,
+ 7.35000344
+ ],
+ "name" : "Wengern Ost"
+ },
+ {
+ "ds100" : "MWBN",
+ "eva" : 8070620,
+ "latlong" : [
+ 49.025162,
+ 10.397122
+ ],
+ "name" : "Wilburgstetten Bf"
+ },
+ {
+ "ds100" : "FSY",
+ "eva" : 8005762,
+ "latlong" : [
+ 51.282472,
+ 8.629398
+ ],
+ "name" : "Willingen-Stryck"
+ },
+ {
+ "ds100" : "XLWW",
+ "eva" : 8271120,
+ "latlong" : [
+ 49.988201,
+ 6.00081
+ ],
+ "name" : "Wilwerwiltz"
+ },
+ {
+ "ds100" : "XPWIT",
+ "eva" : 5100261,
+ "latlong" : [
+ 52.667433,
+ 14.896236
+ ],
+ "name" : "Witnica"
+ },
+ {
+ "ds100" : "XFWT",
+ "eva" : 8702282,
+ "latlong" : [
+ 49.056011,
+ 7.140685
+ ],
+ "name" : "Wittring"
+ },
+ {
+ "ds100" : "XCV",
+ "eva" : 2000021,
+ "latlong" : [
+ 55.197393,
+ 34.317551
+ ],
+ "name" : "Wjasma"
+ },
+ {
+ "ds100" : "XPZD",
+ "eva" : 5100264,
+ "latlong" : [
+ 49.870492,
+ 18.623698
+ ],
+ "name" : "Zebrzydowice"
+ },
+ {
+ "ds100" : "XFZ",
+ "eva" : 8702295,
+ "latlong" : [
+ 49.078197,
+ 7.134154
+ ],
+ "name" : "Zetting"
+ },
+ {
+ "ds100" : "XEZM",
+ "eva" : 7100369,
+ "latlong" : [
+ 43.087052,
+ -2.320028
+ ],
+ "name" : "Zumarraga"
+ }
+]
diff --git a/templates/changelog.html.ep b/templates/changelog.html.ep
index ee3ddbf..0812276 100644
--- a/templates/changelog.html.ep
+++ b/templates/changelog.html.ep
@@ -2,6 +2,19 @@
<div class="row">
<div class="col s12 m1 l1">
+ 1.28
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Behandlung von nicht mehr im IRIS eingepflegten Stationen bei vergangenen Reisen.
+ Bislang hatten diese zu unvollständigen Reisestatistiken geführt.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
1.27
</div>
<div class="col s12 m11 l11">
@@ -219,7 +232,7 @@
href="/account/privacy">Privatsphäre-Einstellungen</a> aktiv ist.
</p>
<p>
- <i class="material-icons left" aria-label="Bugfix">star</i>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
Behandlung von Haltausfällen während der Reise bzw. nach dem Checkin.
</p>
</div>