summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--.gitmodules3
m---------ext/transport-apis0
-rw-r--r--lib/Travel/Status/DE/HAFAS.pm475
-rw-r--r--lib/Travel/Status/DE/HAFAS/Services.pm.PL317
5 files changed, 353 insertions, 444 deletions
diff --git a/.gitignore b/.gitignore
index 697aac5..2111253 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
/cover_db
/MANIFEST*
/MYMETA.*
+
+lib/Travel/Status/DE/HAFAS/Services.pm
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..e1af5dd
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "ext/transport-apis"]
+ path = ext/transport-apis
+ url = https://github.com/public-transport/transport-apis.git
diff --git a/ext/transport-apis b/ext/transport-apis
new file mode 160000
+Subproject 5222788b43582523ebd91390f6fb9416406fee7
diff --git a/lib/Travel/Status/DE/HAFAS.pm b/lib/Travel/Status/DE/HAFAS.pm
index 12bc568..50e33c0 100644
--- a/lib/Travel/Status/DE/HAFAS.pm
+++ b/lib/Travel/Status/DE/HAFAS.pm
@@ -14,436 +14,23 @@ use Digest::MD5 qw(md5_hex);
use Encode qw(decode encode);
use JSON;
use LWP::UserAgent;
+use Math::Polygon;
use Travel::Status::DE::HAFAS::Journey;
use Travel::Status::DE::HAFAS::Location;
use Travel::Status::DE::HAFAS::Message;
use Travel::Status::DE::HAFAS::Polyline qw(decode_polyline);
use Travel::Status::DE::HAFAS::Product;
+use Travel::Status::DE::HAFAS::Services;
use Travel::Status::DE::HAFAS::StopFinder;
our $VERSION = '6.04';
# {{{ Endpoint Definition
-# Most of these have been adapted from
-# <https://github.com/public-transport/transport-apis> and
-# <https://github.com/public-transport/hafas-client/tree/main/p>.
-# Many thanks to Jannis R / @derhuerst and all contributors for maintaining
-# these resources.
-my %hafas_instance = (
- AVV => {
- stopfinder => 'https://auskunft.avv.de/bin/ajax-getstop.exe',
- mgate => 'https://auskunft.avv.de/bin/mgate.exe',
- name => 'Aachener Verkehrsverbund',
- productbits => [
- [ regio => 'region trains' ],
- [ ic_ec => 'long distance trains' ],
- [ ice => 'long distance trains' ],
- [ bus => 'long distance busses' ],
- [ s => 'sububrban trains' ],
- [ u => 'underground trains' ],
- [ tram => 'trams' ],
- [ bus => 'busses' ],
- [ bus => 'additional busses' ],
- [ ondemand => 'on-demand services' ],
- [ ferry => 'maritime transit' ]
- ],
- languages => [qw[de]],
- request => {
- client => {
- id => 'AVV_AACHEN',
- type => 'WEB',
- name => 'webapp',
- l => 'vs_avv',
- },
- ver => '1.26',
- auth => {
- type => 'AID',
- aid => '4vV1AcH3' . 'N511icH',
- },
- lang => 'deu',
- },
- },
- BART => {
- stopfinder => 'https://planner.bart.gov/bin/ajax-getstop.exe',
- mgate => 'https://planner.bart.gov/bin/mgate.exe',
- name => 'Bay Area Rapid Transit',
- time_zone => 'America/Los_Angeles',
- productbits => [
- [ _ => undef ],
- [ _ => undef ],
- [ cc => 'cable cars' ],
- [ regio => 'regional trains' ],
- [ _ => undef ],
- [ bus => 'busses' ],
- [ ferry => 'maritime transit' ],
- [ bart => 'BART trains' ],
- [ tram => 'trams' ],
- ],
- languages => [qw[en]],
- request => {
- client => {
- id => 'BART',
- type => 'WEB',
- name => 'webapp',
- },
- ver => '1.40',
- auth => {
- type => 'AID',
- aid => 'kEwHkFUC' . 'IL500dym',
- },
- lang => 'en',
- },
- },
- BLS => {
- mgate => 'https://bls.hafas.de/bin/mgate.exe',
- stopfinder => 'https://bls.hafas.de/bin/ajax-stopfinder.exe',
- name => 'BLS AG',
- time_zone => 'Europe/Zurich',
- productbits => [
- [ ice => 'long distance trains' ],
- [ ic_ec => 'long distance trains' ],
- [ ir => 'inter-regio trains' ],
- [ regio => 'regional trains' ],
- [ ferry => 'maritime transit' ],
- [ s => 'suburban trains' ],
- [ bus => 'busses' ],
- [ fun => 'funicular / gondola' ],
- [ _ => undef ],
- [ tram => 'trams' ],
- [ _ => undef ],
- [ _ => undef ],
- [ car => 'Autoverlad' ]
- ],
- languages => [qw[de fr it en]],
- request => {
- client => {
- id => 'HAFAS',
- type => 'WEB',
- name => 'webapp',
- },
- ver => '1.46',
- auth => {
- type => 'AID',
- aid => '3jkAncud78HSo' . 'qclmN54812A',
- },
- lang => 'deu',
- },
- },
- BVG => {
- stopfinder => 'https://bvg-apps-ext.hafas.de/bin/ajax-getstop.exe',
- mgate => 'https://bvg-apps-ext.hafas.de/bin/mgate.exe',
- name => 'Berliner Verkehrsbetriebe',
- productbits => [qw[s u tram bus]],
- languages => [qw[de en]],
- request => {
- client => {
- type => 'WEB',
- id => 'VBB',
- v => 10002,
- name => 'webapp',
- l => 'vs_webapp',
- },
- ext => 'BVG.1',
- ver => '1.72',
- auth => {
- type => 'AID',
- aid => 'dVg4TZbW8anjx9z' . 'tPwe2uk4LVRi9wO',
- },
- lang => 'deu',
- },
- },
- CMTA => {
- stopfinder => 'https://capmetro.hafas.cloud/bin/ajax-getstop.exe',
- mgate => 'https://capmetro.hafas.cloud/bin/mgate.exe',
- name => 'Capital Metropolitan Transportation Authority',
- time_zone => 'America/Chicago',
- productbits => [
- [ _ => undef ],
- [ _ => undef ],
- [ _ => undef ],
- [ regio => 'MetroRail' ],
- [ _ => undef ],
- [ bus => 'MetroBus' ],
- [ _ => undef ],
- [ _ => undef ],
- [ _ => undef ],
- [ _ => undef ],
- [ _ => undef ],
- [ _ => undef ],
- [ rapid => 'MetroRapid' ],
- ],
- languages => [qw[en]],
- request => {
- client => {
- id => 'CMTA',
- type => 'IPH',
- name => 'CapMetro',
- v => 2,
- },
- ver => '1.40',
- auth => {
- type => 'AID',
- aid => 'ioslaskd' . 'cndrjcmlsd',
- },
- lang => 'en',
- },
- },
- DB => {
- stopfinder => 'https://reiseauskunft.bahn.de/bin/ajax-getstop.exe',
- mgate => 'https://reiseauskunft.bahn.de/bin/mgate.exe',
- name => 'Deutsche Bahn',
- productbits => [qw[ice ic_ec d regio s bus ferry u tram ondemand]],
- productgroups =>
- [ [qw[ice ic_ec d]], [qw[regio s]], [qw[bus ferry u tram ondemand]] ],
- salt => 'bdI8UVj4' . '0K5fvxwf',
- languages => [qw[de en fr es]],
- request => {
- client => {
- id => 'DB',
- v => '20100000',
- type => 'IPH',
- name => 'DB Navigator',
- },
- ext => 'DB.R21.12.a',
- ver => '1.15',
- auth => {
- type => 'AID',
- aid => 'n91dB8Z77' . 'MLdoR0K'
- },
- },
- },
- IE => {
- stopfinder =>
- 'https://journeyplanner.irishrail.ie/bin/ajax-getstop.exe',
- mgate => 'https://journeyplanner.irishrail.ie/bin/mgate.exe',
- name => 'Iarnród Éireann',
- time_zone => 'Europe/Dublin',
- productbits => [
- [ _ => undef ],
- [ ic => 'national trains' ],
- [ _ => undef ],
- [ regio => 'regional trains' ],
- [ dart => 'DART trains' ],
- [ _ => undef ],
- [ luas => 'LUAS trams' ],
- ],
- languages => [qw[en ga]],
- request => {
- client => {
- id => 'IRISHRAIL',
- type => 'IPA',
- name => 'IrishRailPROD-APPSTORE',
- v => '4000100',
- os => 'iOS 12.4.8',
- },
- ver => '1.33',
- auth => {
- type => 'AID',
- aid => 'P9bplgVCG' . 'nozdgQE',
- },
- lang => 'en',
- },
- salt => 'i5s7m3q9' . 'z6b4k1c2',
- micmac => 1,
- },
- KVB => {
- mgate => 'https://auskunft.kvb.koeln/gate',
- name => 'Kölner Verkehrs-Betriebe',
- productbits => [
- [ s => 'sub-urban trains' ],
- [ tram => 'trams' ],
- [ _ => undef ],
- [ bus => 'buses' ],
- [ regio => 'regional trains' ],
- [ ic => 'national trains' ],
- [ _ => undef ],
- [ _ => undef ],
- [ ondemand => 'taxi buses' ]
- ],
- request => {
- client => {
- id => 'HAFAS',
- type => 'WEB',
- name => 'webapp',
- l => 'vs_webapp',
- v => '154',
- },
- ver => '1.58',
- auth => {
- type => 'AID',
- aid => 'Rt6foY5' . 'zcTTRXMQs',
- },
- lang => 'deu',
- },
- },
- NAHSH => {
- mgate => 'https://nah.sh.hafas.de/bin/mgate.exe',
- stopfinder => 'https://nah.sh.hafas.de/bin/ajax-getstop.exe',
- name => 'Nahverkehrsverbund Schleswig-Holstein',
- productbits => [qw[ice ice ice regio s bus ferry u tram ondemand]],
- request => {
- client => {
- id => 'NAHSH',
- v => '3000700',
- type => 'IPH',
- name => 'NAHSHPROD',
- },
- ver => '1.16',
- auth => {
- type => 'AID',
- aid => 'r0Ot9FLF' . 'NAFxijLW'
- },
- },
- },
- NASA => {
- mgate => 'https://reiseauskunft.insa.de/bin/mgate.exe',
- stopfinder => 'https://reiseauskunft.insa.de/bin/ajax-getstop.exe',
- name => 'Nahverkehrsservice Sachsen-Anhalt',
- productbits => [qw[ice ice regio regio regio tram bus ondemand]],
- languages => [qw[de en]],
- request => {
- client => {
- id => 'NASA',
- v => '4000200',
- type => 'IPH',
- name => 'nasaPROD',
- os => 'iPhone OS 13.1.2',
- },
- ver => '1.18',
- auth => {
- type => 'AID',
- aid => 'nasa-' . 'apps',
- },
- lang => 'deu',
- },
- },
- NVV => {
- mgate => 'https://auskunft.nvv.de/auskunft/bin/app/mgate.exe',
- stopfinder =>
- 'https://auskunft.nvv.de/auskunft/bin/jp/ajax-getstop.exe',
- name => 'Nordhessischer VerkehrsVerbund',
- productbits =>
- [qw[ice ic_ec regio s u tram bus bus ferry ondemand regio regio]],
- request => {
- client => {
- id => 'NVV',
- v => '5000300',
- type => 'IPH',
- name => 'NVVMobilPROD_APPSTORE',
- os => 'iOS 13.1.2',
- },
- ext => 'NVV.6.0',
- ver => '1.18',
- auth => {
- type => 'AID',
- aid => 'Kt8eNOH7' . 'qjVeSxNA',
- },
- lang => 'deu',
- },
- },
- 'ÖBB' => {
- mgate => 'https://fahrplan.oebb.at/bin/mgate.exe',
- stopfinder => 'https://fahrplan.oebb.at/bin/ajax-getstop.exe',
- name => 'Österreichische Bundesbahnen',
- time_zone => 'Europe/Vienna',
- productbits => [
- [ ice_rj => 'long distance trains' ],
- [ sev => 'rail replacement service' ],
- [ ic_ec => 'long distance trains' ],
- [ d_n => 'night trains and rapid trains' ],
- [ regio => 'regional trains' ],
- [ s => 'suburban trains' ],
- [ bus => 'busses' ],
- [ ferry => 'maritime transit' ],
- [ u => 'underground' ],
- [ tram => 'trams' ],
- [ other => 'other transit services' ]
- ],
- productgroups =>
- [ qw[ice_rj ic_ec d_n], qw[regio s sev], qw[bus ferry u tram other] ],
- request => {
- client => {
- id => 'OEBB',
- v => '6030600',
- type => 'IPH',
- name => 'oebbPROD-ADHOC',
- },
- ver => '1.57',
- auth => {
- type => 'AID',
- aid => 'OWDL4fE4' . 'ixNiPBBm',
- },
- lang => 'deu',
- },
- },
- VBB => {
- mgate => 'https://fahrinfo.vbb.de/bin/mgate.exe',
- stopfinder => 'https://fahrinfo.vbb.de/bin/ajax-getstop.exe',
- name => 'Verkehrsverbund Berlin-Brandenburg',
- productbits => [qw[s u tram bus ferry ice regio]],
- languages => [qw[de en]],
- request => {
- client => {
- id => 'VBB',
- type => 'WEB',
- name => 'VBB WebApp',
- l => 'vs_webapp_vbb',
- },
- ext => 'VBB.1',
- ver => '1.33',
- auth => {
- type => 'AID',
- aid => 'hafas-vb' . 'b-webapp',
- },
- lang => 'deu',
- },
- },
- VBN => {
- mgate => 'https://fahrplaner.vbn.de/bin/mgate.exe',
- stopfinder => 'https://fahrplaner.vbn.de/hafas/ajax-getstop.exe',
- name => 'Verkehrsverbund Bremen/Niedersachsen',
- productbits => [qw[ice ice regio regio s bus ferry u tram ondemand]],
- salt => 'SP31mBu' . 'fSyCLmNxp',
- micmac => 1,
- languages => [qw[de en]],
- request => {
- client => {
- id => 'VBN',
- v => '6000000',
- type => 'IPH',
- name => 'vbn',
- },
- ver => '1.42',
- auth => {
- type => 'AID',
- aid => 'kaoxIXLn' . '03zCr2KR',
- },
- lang => 'deu',
- },
- },
- VOS => {
- stopfinder => 'https://fahrplan.vos.info/bin/ajax-getstop.exe',
- mgate => 'https://fahrplan.vos.info/bin/mgate.exe',
- name => 'Verkehrsgemeinschaft Osnabrück',
- productbits => [qw[ice ic_ec d regio s bus ferry u tram ondemand]],
- languages => [qw[de]],
- request => {
- client => {
- id => 'SWO',
- type => 'WEB',
- name => 'webapp',
- l => 'vs_swo',
- },
- ver => '1.72',
- auth => {
- type => 'AID',
- aid => 'PnYowCQ' . 'P7Tp1V'
- },
- lang => 'deu',
- },
- },
-);
+# Data sources: <https://github.com/public-transport/transport-apis> and
+# <https://github.com/public-transport/hafas-client/tree/main/p>. Thanks to
+# Jannis R / @derhuerst and all contributors for maintaining these.
+my $hafas_instance = Travel::Status::DE::HAFAS::Services::get_service_ref();
# }}}
# {{{ Constructors
@@ -477,11 +64,11 @@ sub new {
$service = $conf{service} = 'DB';
}
- if ( defined $service and not exists $hafas_instance{$service} ) {
+ if ( defined $service and not exists $hafas_instance->{$service} ) {
confess("The service '$service' is not supported");
}
- my $now = DateTime->now( time_zone => $hafas_instance{$service}{time_zone}
+ my $now = DateTime->now( time_zone => $hafas_instance->{$service}{time_zone}
// 'Europe/Berlin' );
my $self = {
active_service => $service,
@@ -514,7 +101,7 @@ sub new {
},
}
],
- %{ $hafas_instance{$service}{request} }
+ %{ $hafas_instance->{$service}{request} }
};
}
elsif ( $conf{journeyMatch} ) {
@@ -535,7 +122,7 @@ sub new {
},
}
],
- %{ $hafas_instance{$service}{request} }
+ %{ $hafas_instance->{$service}{request} }
};
}
elsif ( $conf{geoSearch} ) {
@@ -566,7 +153,7 @@ sub new {
}
}
],
- %{ $hafas_instance{$service}{request} }
+ %{ $hafas_instance->{$service}{request} }
};
}
elsif ( $conf{locationSearch} ) {
@@ -587,7 +174,7 @@ sub new {
}
}
],
- %{ $hafas_instance{$service}{request} }
+ %{ $hafas_instance->{$service}{request} }
};
}
else {
@@ -627,7 +214,7 @@ sub new {
},
},
],
- %{ $hafas_instance{$service}{request} }
+ %{ $hafas_instance->{$service}{request} }
};
}
@@ -637,7 +224,7 @@ sub new {
$self->{strptime_obj} //= DateTime::Format::Strptime->new(
pattern => '%Y%m%dT%H%M%S',
- time_zone => $hafas_instance{$service}{time_zone} // 'Europe/Berlin',
+ time_zone => $hafas_instance->{$service}{time_zone} // 'Europe/Berlin',
);
my $json = $self->{json} = JSON->new->utf8;
@@ -651,10 +238,10 @@ sub new {
$req = $json->encode($req);
$self->{post} = $req;
- my $url = $conf{url} // $hafas_instance{$service}{mgate};
+ my $url = $conf{url} // $hafas_instance->{$service}{mgate};
- if ( my $salt = $hafas_instance{$service}{salt} ) {
- if ( $hafas_instance{$service}{micmac} ) {
+ if ( my $salt = $hafas_instance->{$service}{salt} ) {
+ if ( $hafas_instance->{$service}{micmac} ) {
my $mic = md5_hex( $self->{post} );
my $mac = md5_hex( $mic . $salt );
$url .= "?mic=$mic&mac=$mac";
@@ -772,15 +359,15 @@ sub mot_mask {
my ($self) = @_;
my $service = $self->{active_service};
- my $mot_mask = 2**@{ $hafas_instance{$service}{productbits} } - 1;
+ my $mot_mask = 2**@{ $hafas_instance->{$service}{productbits} } - 1;
my %mot_pos;
- for my $i ( 0 .. $#{ $hafas_instance{$service}{productbits} } ) {
- if ( ref( $hafas_instance{$service}{productbits}[$i] ) eq 'ARRAY' ) {
- $mot_pos{ $hafas_instance{$service}{productbits}[$i][0] } = $i;
+ for my $i ( 0 .. $#{ $hafas_instance->{$service}{productbits} } ) {
+ if ( ref( $hafas_instance->{$service}{productbits}[$i] ) eq 'ARRAY' ) {
+ $mot_pos{ $hafas_instance->{$service}{productbits}[$i][0] } = $i;
}
else {
- $mot_pos{ $hafas_instance{$service}{productbits}[$i] } = $i;
+ $mot_pos{ $hafas_instance->{$service}{productbits}[$i] } = $i;
}
}
@@ -1129,10 +716,10 @@ sub similar_stops {
my $service = $self->{active_service};
- if ( $service and exists $hafas_instance{$service}{stopfinder} ) {
+ if ( $service and exists $hafas_instance->{$service}{stopfinder} ) {
my $sf = Travel::Status::DE::HAFAS::StopFinder->new(
- url => $hafas_instance{$service}{stopfinder},
+ url => $hafas_instance->{$service}{stopfinder},
input => $self->{station},
ua => $self->{ua},
developer_mode => $self->{developer_mode},
@@ -1151,11 +738,11 @@ sub similar_stops_p {
my $service = $self->{active_service};
- if ( $service and exists $hafas_instance{$service}{stopfinder} ) {
+ if ( $service and exists $hafas_instance->{$service}{stopfinder} ) {
$opt{user_agent} //= $self->{ua};
$opt{promise} //= $self->{promise};
return Travel::Status::DE::HAFAS::StopFinder->new_p(
- url => $hafas_instance{$service}{stopfinder},
+ url => $hafas_instance->{$service}{stopfinder},
input => $self->{station},
user_agent => $opt{user_agent},
developer_mode => $self->{developer_mode},
@@ -1237,8 +824,8 @@ sub result {
# static
sub get_services {
my @services;
- for my $service ( sort keys %hafas_instance ) {
- my %desc = %{ $hafas_instance{$service} };
+ for my $service ( sort keys %{$hafas_instance} ) {
+ my %desc = %{ $hafas_instance->{$service} };
$desc{shortname} = $service;
push( @services, \%desc );
}
@@ -1249,8 +836,8 @@ sub get_services {
sub get_service {
my ($service) = @_;
- if ( defined $service and exists $hafas_instance{$service} ) {
- return $hafas_instance{$service};
+ if ( defined $service and exists $hafas_instance->{$service} ) {
+ return $hafas_instance->{$service};
}
return;
}
@@ -1259,7 +846,7 @@ sub get_active_service {
my ($self) = @_;
if ( defined $self->{active_service} ) {
- return $hafas_instance{ $self->{active_service} };
+ return $hafas_instance->{ $self->{active_service} };
}
return;
}
diff --git a/lib/Travel/Status/DE/HAFAS/Services.pm.PL b/lib/Travel/Status/DE/HAFAS/Services.pm.PL
new file mode 100644
index 0000000..9cda569
--- /dev/null
+++ b/lib/Travel/Status/DE/HAFAS/Services.pm.PL
@@ -0,0 +1,317 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use 5.014;
+use utf8;
+use Data::Dumper;
+use Encode qw(encode);
+use File::Slurp qw(read_file write_file);
+use JSON;
+
+my $json = JSON->new->utf8;
+
+sub load_instance {
+ my ( $path, %opt ) = @_;
+
+ my $data = $json->decode(
+ scalar read_file("ext/transport-apis/data/${path}-hafas-mgate.json") );
+ my %ret = (
+ name => $data->{name} =~ s{ *[(][^)]+[)]}{}r,
+ homepage => $data->{attribution}{homepage},
+ mgate => $data->{options}{endpoint},
+ time_zone => $data->{timezone},
+ languages => $data->{supportedLanguages},
+ request => {
+ client => $data->{options}{client},
+ auth => $data->{options}{auth},
+ },
+
+ #coverage => {
+ # area => $data->{coverage}{realtimeCoverage}{area},
+ # regions => $data->{coverage}{realtimeCoverage}{region},
+ #}
+ );
+
+ if ( $opt{lang} ) {
+ $ret{request}{lang} = $opt{lang};
+ }
+ if ( $opt{ver} ) {
+ $ret{request}{ver} = $opt{ver};
+ }
+
+ if ( $data->{options}{ext} ) {
+ $ret{request}{ext} = $data->{options}{ext};
+ }
+ if ( $data->{options}{ver} ) {
+ $ret{request}{ver} = $data->{options}{ver};
+ }
+
+ return %ret;
+}
+
+my %hafas_instance = (
+ AVV => {
+ load_instance(
+ 'de/avv',
+ lang => 'deu',
+ ver => '1.26'
+ ),
+ stopfinder => 'https://auskunft.avv.de/bin/ajax-getstop.exe',
+ productbits => [
+ [ regio => 'regional trains' ],
+ [ ic_ec => 'long distance trains' ],
+ [ ice => 'long distance trains' ],
+ [ bus => 'long distance busses' ],
+ [ s => 'sububrban trains' ],
+ [ u => 'underground trains' ],
+ [ tram => 'trams' ],
+ [ bus => 'busses' ],
+ [ bus => 'additional busses' ],
+ [ ondemand => 'on-demand services' ],
+ [ ferry => 'maritime transit' ]
+ ],
+ },
+ BART => {
+ load_instance(
+ 'us/bart',
+ lang => 'en',
+ ver => '1.40'
+ ),
+ stopfinder => 'https://planner.bart.gov/bin/ajax-getstop.exe',
+ productbits => [
+ [ _ => undef ],
+ [ _ => undef ],
+ [ cc => 'cable cars' ],
+ [ regio => 'regional trains' ],
+ [ _ => undef ],
+ [ bus => 'busses' ],
+ [ ferry => 'maritime transit' ],
+ [ bart => 'BART trains' ],
+ [ tram => 'trams' ],
+ ],
+ },
+ BLS => {
+ load_instance(
+ 'ch/bls',
+ lang => 'deu',
+ ver => '1.46'
+ ),
+ languages => [qw[de fr it en]],
+ stopfinder => 'https://bls.hafas.de/bin/ajax-stopfinder.exe',
+ productbits => [
+ [ ice => 'long distance trains' ],
+ [ ic_ec => 'long distance trains' ],
+ [ ir => 'inter-regio trains' ],
+ [ regio => 'regional trains' ],
+ [ ferry => 'maritime transit' ],
+ [ s => 'suburban trains' ],
+ [ bus => 'busses' ],
+ [ fun => 'funicular / gondola' ],
+ [ _ => undef ],
+ [ tram => 'trams' ],
+ [ _ => undef ],
+ [ _ => undef ],
+ [ car => 'Autoverlad' ]
+ ],
+ },
+ BVG => {
+ load_instance( 'de/bvg', lang => 'deu' ),
+ stopfinder => 'https://bvg-apps-ext.hafas.de/bin/ajax-getstop.exe',
+ productbits => [qw[s u tram bus]],
+ },
+ CMTA => {
+ load_instance(
+ 'us/cmta',
+ lang => 'en',
+ ver => '1.40'
+ ),
+ stopfinder => 'https://capmetro.hafas.cloud/bin/ajax-getstop.exe',
+ productbits => [
+ [ _ => undef ],
+ [ _ => undef ],
+ [ _ => undef ],
+ [ regio => 'MetroRail' ],
+ [ _ => undef ],
+ [ bus => 'MetroBus' ],
+ [ _ => undef ],
+ [ _ => undef ],
+ [ _ => undef ],
+ [ _ => undef ],
+ [ _ => undef ],
+ [ _ => undef ],
+ [ rapid => 'MetroRapid' ],
+ ],
+ },
+ DB => {
+ load_instance('de/db'),
+ stopfinder => 'https://reiseauskunft.bahn.de/bin/ajax-getstop.exe',
+ productbits => [qw[ice ic_ec d regio s bus ferry u tram ondemand]],
+ productgroups =>
+ [ [qw[ice ic_ec d]], [qw[regio s]], [qw[bus ferry u tram ondemand]] ],
+ salt => 'bdI8UVj40K5fvxwf',
+ languages => [qw[de en fr es]],
+ request => {
+ client => {
+ id => 'DB',
+ v => '20100000',
+ type => 'IPH',
+ name => 'DB Navigator',
+ },
+ ext => 'DB.R21.12.a',
+ ver => '1.18',
+ auth => {
+ type => 'AID',
+ aid => 'n91dB8Z77MLdoR0K'
+ },
+ lang => 'deu'
+ },
+ },
+ IE => {
+ load_instance(
+ 'ie/iarnrod-eireann',
+ lang => 'en',
+ ver => '1.33'
+ ),
+ stopfinder =>
+ 'https://journeyplanner.irishrail.ie/bin/ajax-getstop.exe',
+ productbits => [
+ [ _ => undef ],
+ [ ic => 'national trains' ],
+ [ _ => undef ],
+ [ regio => 'regional trains' ],
+ [ dart => 'DART trains' ],
+ [ _ => undef ],
+ [ luas => 'LUAS trams' ],
+ ],
+ salt => 'i5s7m3q9z6b4k1c2',
+ micmac => 1,
+ },
+ KVB => {
+ load_instance( 'de/kvb', lang => 'deu' ),
+ productbits => [
+ [ s => 'sub-urban trains' ],
+ [ tram => 'trams' ],
+ [ _ => undef ],
+ [ bus => 'buses' ],
+ [ regio => 'regional trains' ],
+ [ ic => 'national trains' ],
+ [ _ => undef ],
+ [ _ => undef ],
+ [ ondemand => 'taxi buses' ]
+ ],
+ },
+ NAHSH => {
+ load_instance('de/nahsh'),
+ stopfinder => 'https://nah.sh.hafas.de/bin/ajax-getstop.exe',
+ productbits => [qw[ice ice ice regio s bus ferry u tram ondemand]],
+ },
+ NASA => {
+ load_instance( 'de/nasa', lang => 'deu' ),
+ stopfinder => 'https://reiseauskunft.insa.de/bin/ajax-getstop.exe',
+ productbits => [qw[ice ice regio regio regio tram bus ondemand]],
+ },
+ NVV => {
+ load_instance( 'de/nvv', lang => 'deu' ),
+ stopfinder =>
+ 'https://auskunft.nvv.de/auskunft/bin/jp/ajax-getstop.exe',
+ productbits =>
+ [qw[ice ic_ec regio s u tram bus bus ferry ondemand regio regio]],
+ },
+ 'ÖBB' => {
+ load_instance(
+ 'at/oebb',
+ lang => 'deu',
+ ver => '1.57'
+ ),
+ stopfinder => 'https://fahrplan.oebb.at/bin/ajax-getstop.exe',
+ productbits => [
+ [ ice_rj => 'long distance trains' ],
+ [ sev => 'rail replacement service' ],
+ [ ic_ec => 'long distance trains' ],
+ [ d_n => 'night trains and rapid trains' ],
+ [ regio => 'regional trains' ],
+ [ s => 'suburban trains' ],
+ [ bus => 'busses' ],
+ [ ferry => 'maritime transit' ],
+ [ u => 'underground' ],
+ [ tram => 'trams' ],
+ [ other => 'other transit services' ]
+ ],
+ productgroups =>
+ [ qw[ice_rj ic_ec d_n], qw[regio s sev], qw[bus ferry u tram other] ],
+ },
+ VBB => {
+ load_instance( 'de/vbb', lang => 'deu' ),
+ stopfinder => 'https://fahrinfo.vbb.de/bin/ajax-getstop.exe',
+ productbits => [qw[s u tram bus ferry ice regio]],
+ },
+ VBN => {
+ load_instance(
+ 'de/vbn',
+ lang => 'deu',
+ ver => '1.42'
+ ),
+ stopfinder => 'https://fahrplaner.vbn.de/hafas/ajax-getstop.exe',
+ productbits => [qw[ice ice regio regio s bus ferry u tram ondemand]],
+ salt => 'SP31mBufSyCLmNxp',
+ micmac => 1,
+ },
+ VOS => {
+ load_instance(
+ 'de/vos',
+ lang => 'deu',
+ ver => '1.72'
+ ),
+ stopfinder => 'https://fahrplan.vos.info/bin/ajax-getstop.exe',
+ productbits => [qw[ice ic_ec d regio s bus ferry u tram ondemand]],
+ },
+);
+
+my $perlobj = Data::Dumper->new( [ \%hafas_instance ], ['hafas_instance'] );
+
+my $buf = <<'__EOF__';
+package Travel::Status::DE::HAFAS::Services;
+
+# vim:readonly
+# This module has been automatically generated
+# by lib/Travel/Status/DE/HAFAS/Services.pm.PL.
+# Do not edit, changes will be lost.
+
+use strict;
+use warnings;
+use 5.014;
+use utf8;
+
+our $VERSION = '6.04';
+
+# Most of these have been adapted from
+# <https://github.com/public-transport/transport-apis> and
+# <https://github.com/public-transport/hafas-client/tree/main/p>.
+# Many thanks to Jannis R / @derhuerst and all contributors for maintaining
+# these resources.
+
+__EOF__
+
+$buf .= 'my ' . $perlobj->Sortkeys(1)->Indent(1)->Dump;
+
+$buf =~ s{\Q\x{d6}\E}{Ö}g;
+$buf =~ s{\Q\x{c9}\E}{É}g;
+$buf =~ s{\Q\x{f3}\E}{ó}g;
+$buf =~ s{\Q\x{f6}\E}{ö}g;
+$buf =~ s{\Q\x{fc}\E}{ü}g;
+
+$buf .= <<'__EOF__';
+
+sub get_service_ref {
+ return $hafas_instance;
+}
+
+sub get_service_map {
+ return %{$hafas_instance};
+}
+
+1;
+__EOF__
+
+write_file( $ARGV[0], { binmode => ':utf8' }, $buf );