From 2f9ba6e0177dbf641c459a7d89528ed561bff6f8 Mon Sep 17 00:00:00 2001 From: Daniel Friesel Date: Wed, 7 Dec 2022 19:42:48 +0100 Subject: switch to internal station database; add out-of-service stations for old journeys --- README.md | 3 +- lib/Travelynx.pm | 132 +- lib/Travelynx/Command/database.pm | 239 +++- lib/Travelynx/Controller/Api.pm | 26 +- lib/Travelynx/Helper/IRIS.pm | 1 + lib/Travelynx/Model/Journeys.pm | 116 +- lib/Travelynx/Model/Stations.pm | 88 ++ share/old_stations.json | 2603 +++++++++++++++++++++++++++++++++++++ templates/changelog.html.ep | 15 +- 9 files changed, 3035 insertions(+), 188 deletions(-) create mode 100644 lib/Travelynx/Model/Stations.pm create mode 100644 share/old_stations.json 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, ); } ); @@ -409,6 +368,14 @@ sub startup { } ); + $self->helper( + stations => sub { + my ($self) = @_; + state $stations + = Travelynx::Model::Stations->new( pg => $self->pg ); + } + ); + $self->helper( users => sub { my ($self) = @_; @@ -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 @@ -1,5 +1,18 @@

Changelog

+
+
+ 1.28 +
+
+

+ build + Behandlung von nicht mehr im IRIS eingepflegten Stationen bei vergangenen Reisen. + Bislang hatten diese zu unvollständigen Reisestatistiken geführt. +

+
+
+
1.27 @@ -219,7 +232,7 @@ href="/account/privacy">Privatsphäre-Einstellungen aktiv ist.

- star + build Behandlung von Haltausfällen während der Reise bzw. nach dem Checkin.

-- cgit v1.2.3