Unverified Commit 3fb67437 authored by Birte Kristina Friesel's avatar Birte Kristina Friesel
Browse files
parent 163665fd
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -4,3 +4,5 @@
/cover_db
/MANIFEST*
/MYMETA.*

lib/Travel/Status/DE/HAFAS/Services.pm

.gitmodules

0 → 100644
+3 −0
Original line number Diff line number Diff line
[submodule "ext/transport-apis"]
	path = ext/transport-apis
	url = https://github.com/public-transport/transport-apis.git
Original line number Diff line number Diff line
Subproject commit 5222788b43582523ebd91390f6fb9416406fee78
+31 −444
Original line number Diff line number Diff line
@@ -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;
}
+317 −0
Original line number Diff line number Diff line
#!/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 );