summaryrefslogtreecommitdiff
path: root/lib/Travel/Status/DE/HAFAS.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/Travel/Status/DE/HAFAS.pm')
-rw-r--r--lib/Travel/Status/DE/HAFAS.pm1434
1 files changed, 1192 insertions, 242 deletions
diff --git a/lib/Travel/Status/DE/HAFAS.pm b/lib/Travel/Status/DE/HAFAS.pm
index 62a123b..19d633c 100644
--- a/lib/Travel/Status/DE/HAFAS.pm
+++ b/lib/Travel/Status/DE/HAFAS.pm
@@ -1,245 +1,1043 @@
package Travel::Status::DE::HAFAS;
+# vim:foldmethod=marker
+
use strict;
use warnings;
use 5.014;
use utf8;
-no if $] >= 5.018, warnings => 'experimental::smartmatch';
-
use Carp qw(confess);
use DateTime;
use DateTime::Format::Strptime;
+use Digest::MD5 qw(md5_hex);
+use Encode qw(decode encode);
+use JSON;
use LWP::UserAgent;
-use POSIX qw(strftime);
-use Travel::Status::DE::HAFAS::Result;
+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::StopFinder;
-use XML::LibXML;
-our $VERSION = '2.03';
+our $VERSION = '6.03';
+
+# {{{ 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 = (
- BVG => {
- url => 'http://bvg.hafas.de/bin/stboard.exe',
- stopfinder => 'http://bvg.hafas.de/bin/ajax-getstop.exe',
- name => 'Berliner Verkehrsgesellschaft',
- productbits => [qw[s u tram bus ferry ice regio ondemand]],
+ 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',
+ },
+ },
+ 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 => {
- url => 'https://reiseauskunft.bahn.de/bin/bhftafel.exe',
- stopfinder => 'https://reiseauskunft.bahn.de/bin/ajax-getstop.exe',
- name => 'Deutsche Bahn',
- productbits =>
- [qw[ice ic_ec d regio s bus ferry u tram ondemand x x x x]],
+ 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,
},
NAHSH => {
- url => 'http://nah.sh.hafas.de/bin/stboard.exe',
- stopfinder => 'http://nah.sh.hafas.de/bin/ajax-getstop.exe',
+ 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 => {
- url => 'http://reiseauskunft.insa.de/bin/stboard.exe',
- stopfinder => 'http://reiseauskunft.insa.de/bin/ajax-getstop.exe',
+ 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 => {
- url => 'http://auskunft.nvv.de/auskunft/bin/jp/stboard.exe',
- stopfinder => 'http://auskunft.nvv.de/auskunft/bin/jp/ajax-getstop.exe',
- name => 'Nordhessischer VerkehrsVerbund',
+ 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' => {
- url => 'http://fahrplan.oebb.at/bin/stboard.exe',
- stopfinder => 'http://fahrplan.oebb.at/bin/ajax-getstop.exe',
- name => 'Österreichische Bundesbahnen',
- productbits =>
- [qw[ice ice ice regio regio s bus ferry u tram ice ondemand ice]],
- },
- RSAG => {
- url => 'http://fahrplan.rsag-online.de/hafas/stboard.exe',
- stopfinder => 'http://fahrplan.rsag-online.de/hafas/ajax-getstop.exe',
- name => 'Rostocker Straßenbahn AG',
- productbits => [qw[ice ice ice regio s bus ferry u tram ondemand]],
- },
- SBB => {
- url => 'http://fahrplan.sbb.ch/bin/stboard.exe',
- stopfinder => 'http://fahrplan.sbb.ch/bin/ajax-getstop.exe',
- name => 'Schweizerische Bundesbahnen',
- productbits =>
- [qw[ice ice regio regio ferry s bus cablecar regio tram]],
+ 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 => {
- url => 'http://fahrinfo.vbb.de/bin/stboard.exe',
- stopfinder => 'http://fahrinfo.vbb.de/bin/ajax-getstop.exe',
+ 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 => {
- url => 'https://fahrplaner.vbn.de/hafas/stboard.exe',
+ 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',
+ },
},
);
+# }}}
+# {{{ Constructors
+
sub new {
my ( $obj, %conf ) = @_;
-
- my $date = $conf{date} // strftime( '%d.%m.%Y', localtime(time) );
- my $time = $conf{time} // strftime( '%H:%M', localtime(time) );
- my $lang = $conf{language} // 'd';
- my $mode = $conf{mode} // 'dep';
my $service = $conf{service};
- my %lwp_options = %{ $conf{lwp_options} // { timeout => 10 } };
-
- my $ua = LWP::UserAgent->new(%lwp_options);
+ my $ua = $conf{user_agent};
- $ua->env_proxy;
-
- my $reply;
+ if ( not $ua ) {
+ my %lwp_options = %{ $conf{lwp_options} // { timeout => 10 } };
+ $ua = LWP::UserAgent->new(%lwp_options);
+ $ua->env_proxy;
+ }
- if ( not $conf{station} ) {
- confess('You need to specify a station');
+ if (
+ not( $conf{station}
+ or $conf{journey}
+ or $conf{journeyMatch}
+ or $conf{geoSearch}
+ or $conf{locationSearch} )
+ )
+ {
+ confess(
+'station / journey / journeyMatch / geoSearch / locationSearch must be specified'
+ );
}
- if ( not defined $service and not defined $conf{url} ) {
- $service = 'DB';
+ if ( not defined $service ) {
+ $service = $conf{service} = 'DB';
}
if ( defined $service and not exists $hafas_instance{$service} ) {
confess("The service '$service' is not supported");
}
- my $ref = {
+ my $now = DateTime->now( time_zone => $hafas_instance{$service}{time_zone}
+ // 'Europe/Berlin' );
+ my $self = {
active_service => $service,
+ arrivals => $conf{arrivals},
+ cache => $conf{cache},
developer_mode => $conf{developer_mode},
exclusive_mots => $conf{exclusive_mots},
excluded_mots => $conf{excluded_mots},
+ messages => [],
+ results => [],
station => $conf{station},
ua => $ua,
- post => {
- input => $conf{station},
- date => $date,
- time => $time,
- start => 'yes', # value doesn't matter, just needs to be set
- boardType => $mode,
- L => 'vs_java3',
- },
+ now => $now,
+ tz_offset => $now->offset / 60,
};
- bless( $ref, $obj );
+ bless( $self, $obj );
+
+ my $req;
+
+ if ( $conf{journey} ) {
+ $req = {
+ svcReqL => [
+ {
+ meth => 'JourneyDetails',
+ req => {
+ jid => $conf{journey}{id},
+ name => $conf{journey}{name} // '0',
+ getPolyline => $conf{with_polyline} ? \1 : \0,
+ },
+ }
+ ],
+ %{ $hafas_instance{$service}{request} }
+ };
+ }
+ elsif ( $conf{journeyMatch} ) {
+ $req = {
+ svcReqL => [
+ {
+ meth => 'JourneyMatch',
+ req => {
+ date => ( $conf{datetime} // $now )->strftime('%Y%m%d'),
+ input => $conf{journeyMatch},
+ jnyFltrL => [
+ {
+ type => "PROD",
+ mode => "INC",
+ value => $self->mot_mask
+ }
+ ]
+ },
+ }
+ ],
+ %{ $hafas_instance{$service}{request} }
+ };
+ }
+ elsif ( $conf{geoSearch} ) {
+ $req = {
+ svcReqL => [
+ {
+ cfg => { polyEnc => 'GPA' },
+ meth => 'LocGeoPos',
+ req => {
+ ring => {
+ cCrd => {
+ x => int( $conf{geoSearch}{lon} * 1e6 ),
+ y => int( $conf{geoSearch}{lat} * 1e6 ),
+ },
+ maxDist => -1,
+ minDist => 0,
+ },
+ locFltrL => [
+ {
+ type => "PROD",
+ mode => "INC",
+ value => $self->mot_mask
+ }
+ ],
+ getPOIs => \0,
+ getStops => \1,
+ maxLoc => $conf{results} // 30,
+ }
+ }
+ ],
+ %{ $hafas_instance{$service}{request} }
+ };
+ }
+ elsif ( $conf{locationSearch} ) {
+ $req = {
+ svcReqL => [
+ {
+ cfg => { polyEnc => 'GPA' },
+ meth => 'LocMatch',
+ req => {
+ input => {
+ loc => {
+ type => 'S',
+ name => $conf{locationSearch},
+ },
+ maxLoc => $conf{results} // 30,
+ field => 'S',
+ },
+ }
+ }
+ ],
+ %{ $hafas_instance{$service}{request} }
+ };
+ }
+ else {
+ my $date = ( $conf{datetime} // $now )->strftime('%Y%m%d');
+ my $time = ( $conf{datetime} // $now )->strftime('%H%M%S');
- $ref->set_productfilter;
+ my $lid;
+ if ( $self->{station} =~ m{ ^ [0-9]+ $ }x ) {
+ $lid = 'A=1@L=' . $self->{station} . '@';
+ }
+ else {
+ $lid = 'A=1@O=' . $self->{station} . '@';
+ }
- my $url = ( $conf{url} // $hafas_instance{$service}{url} ) . "/${lang}n";
+ my $maxjny = $conf{results} // 30;
+ my $duration = $conf{lookahead} // -1;
+
+ $req = {
+ svcReqL => [
+ {
+ meth => 'StationBoard',
+ req => {
+ type => ( $conf{arrivals} ? 'ARR' : 'DEP' ),
+ stbLoc => { lid => $lid },
+ dirLoc => undef,
+ maxJny => $maxjny,
+ date => $date,
+ time => $time,
+ dur => $duration,
+ jnyFltrL => [
+ {
+ type => "PROD",
+ mode => "INC",
+ value => $self->mot_mask
+ }
+ ]
+ },
+ },
+ ],
+ %{ $hafas_instance{$service}{request} }
+ };
+ }
- $reply = $ua->post( $url, $ref->{post} );
+ if ( $conf{language} ) {
+ $req->{lang} = $conf{language};
+ }
- if ( $reply->is_error ) {
- $ref->{errstr} = $reply->status_line;
- return $ref;
+ $self->{strptime_obj} //= DateTime::Format::Strptime->new(
+ pattern => '%Y%m%dT%H%M%S',
+ time_zone => $hafas_instance{$service}{time_zone} // 'Europe/Berlin',
+ );
+
+ my $json = $self->{json} = JSON->new->utf8;
+
+ # The JSON request is the cache key, so if we have a cache we must ensure
+ # that JSON serialization is deterministic.
+ if ( $self->{cache} ) {
+ $json->canonical;
}
- $ref->{raw_xml} = $reply->content;
+ $req = $json->encode($req);
+ $self->{post} = $req;
- # the interface often does not return valid XML (but it's close!)
- if ( substr( $ref->{raw_xml}, 0, 5 ) ne '<?xml' ) {
- $ref->{raw_xml}
- = '<?xml version="1.0" encoding="iso-8859-15"?><wrap>'
- . $ref->{raw_xml}
- . '</wrap>';
+ my $url = $conf{url} // $hafas_instance{$service}{mgate};
+
+ 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";
+ }
+ else {
+ $url .= '?checksum=' . md5_hex( $self->{post} . $salt );
+ }
}
- if ( defined $service and $service =~ m{ ^ VBB | NVV $ }x ) {
+ if ( $conf{async} ) {
+ $self->{url} = $url;
+ return $self;
+ }
- # Returns invalid XML with tags inside HIMMessage's lead attribute.
- # Fix this.
- $ref->{raw_xml}
- =~ s{ lead = " \K ( [^"]+ ) }{ $1 =~ s{ < [^>]+ > }{}grx }egx;
+ if ( $conf{json} ) {
+ $self->{raw_json} = $conf{json};
}
+ else {
+ if ( $self->{developer_mode} ) {
+ say "requesting $req from $url";
+ }
- # TODO the DB backend also retuns invalid XML (similar to above, but with
- # errors in delay="...") when setting the language to dutch/italian.
- # No, I don't know why.
+ my ( $content, $error ) = $self->post_with_cache($url);
- if ( $ref->{developer_mode} ) {
- say $ref->{raw_xml};
+ if ($error) {
+ $self->{errstr} = $error;
+ return $self;
+ }
+
+ if ( $self->{developer_mode} ) {
+ say decode( 'utf-8', $content );
+ }
+
+ $self->{raw_json} = $json->decode($content);
}
- $ref->{tree} = XML::LibXML->load_xml(
- string => $ref->{raw_xml},
- );
+ $self->check_mgate;
- if ( $ref->{developer_mode} ) {
- say $ref->{tree}->toString(1);
+ if ( $conf{journey} ) {
+ $self->parse_journey;
+ }
+ elsif ( $conf{journeyMatch} ) {
+ $self->parse_journey_match;
+ }
+ elsif ( $conf{geoSearch} or $conf{locationSearch} ) {
+ $self->parse_search;
+ }
+ else {
+ $self->parse_board;
}
- $ref->check_input_error;
- return $ref;
+ return $self;
}
-sub set_productfilter {
- my ($self) = @_;
+sub new_p {
+ my ( $obj, %conf ) = @_;
+ my $promise = $conf{promise}->new;
+
+ if (
+ not( $conf{station}
+ or $conf{journey}
+ or $conf{journeyMatch}
+ or $conf{geoSearch}
+ or $conf{locationSearch} )
+ )
+ {
+ return $promise->reject(
+'station / journey / journeyMatch / geoSearch / locationSearch flag must be passed'
+ );
+ }
- my $service = $self->{active_service};
- my $mot_default = '1';
+ my $self = $obj->new( %conf, async => 1 );
+ $self->{promise} = $conf{promise};
- if ( not $service or not exists $hafas_instance{$service}{productbits} ) {
- return;
- }
+ $self->post_with_cache_p( $self->{url} )->then(
+ sub {
+ my ($content) = @_;
+ $self->{raw_json} = $self->{json}->decode($content);
+ $self->check_mgate;
+ if ( $conf{journey} ) {
+ $self->parse_journey;
+ }
+ elsif ( $conf{journeyMatch} ) {
+ $self->parse_journey_match;
+ }
+ elsif ( $conf{geoSearch} or $conf{locationSearch} ) {
+ $self->parse_search;
+ }
+ else {
+ $self->parse_board;
+ }
+ if ( $self->errstr ) {
+ $promise->reject( $self->errstr, $self );
+ }
+ else {
+ $promise->resolve($self);
+ }
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+# }}}
+# {{{ Internal Helpers
+
+sub mot_mask {
+ my ($self) = @_;
+
+ my $service = $self->{active_service};
+ my $mot_mask = 2**@{ $hafas_instance{$service}{productbits} } - 1;
my %mot_pos;
for my $i ( 0 .. $#{ $hafas_instance{$service}{productbits} } ) {
- $mot_pos{ $hafas_instance{$service}{productbits}[$i] } = $i;
+ 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;
+ }
}
- if ( $self->{exclusive_mots} and @{ $self->{exclusive_mots} } ) {
- $mot_default = '0';
+ if ( my @mots = @{ $self->{exclusive_mots} // [] } ) {
+ $mot_mask = 0;
+ for my $mot (@mots) {
+ if ( exists $mot_pos{$mot} ) {
+ $mot_mask |= 1 << $mot_pos{$mot};
+ }
+ }
}
- $self->{post}{productsFilter}
- = $mot_default x ( scalar @{ $hafas_instance{$service}{productbits} } );
-
- if ( $self->{exclusive_mots} and @{ $self->{exclusive_mots} } ) {
- for my $mot ( @{ $self->{exclusive_mots} } ) {
+ if ( my @mots = @{ $self->{excluded_mots} // [] } ) {
+ for my $mot (@mots) {
if ( exists $mot_pos{$mot} ) {
- substr( $self->{post}{productsFilter}, $mot_pos{$mot}, 1, '1' );
+ $mot_mask &= ~( 1 << $mot_pos{$mot} );
}
}
}
- if ( $self->{excluded_mots} and @{ $self->{excluded_mots} } ) {
- for my $mot ( @{ $self->{excluded_mots} } ) {
- if ( exists $mot_pos{$mot} ) {
- substr( $self->{post}{productsFilter}, $mot_pos{$mot}, 1, '0' );
+ return $mot_mask;
+}
+
+sub post_with_cache {
+ my ( $self, $url ) = @_;
+ my $cache = $self->{cache};
+
+ if ( $self->{developer_mode} ) {
+ say "POST $url";
+ }
+
+ if ($cache) {
+ my $content = $cache->thaw( $self->{post} );
+ if ($content) {
+ if ( $self->{developer_mode} ) {
+ say ' cache hit';
}
+ return ( ${$content}, undef );
}
}
- return;
+ if ( $self->{developer_mode} ) {
+ say ' cache miss';
+ }
+
+ my $reply = $self->{ua}->post(
+ $url,
+ 'Content-Type' => 'application/json',
+ Content => $self->{post}
+ );
+
+ if ( $reply->is_error ) {
+ return ( undef, $reply->status_line );
+ }
+ my $content = $reply->content;
+
+ if ($cache) {
+ say "freeeez";
+ $cache->freeze( $self->{post}, \$content );
+ }
+
+ return ( $content, undef );
}
-sub check_input_error {
- my ($self) = @_;
+sub post_with_cache_p {
+ my ( $self, $url ) = @_;
+ my $cache = $self->{cache};
+
+ if ( $self->{developer_mode} ) {
+ say "POST $url";
+ }
+
+ my $promise = $self->{promise}->new;
+
+ if ($cache) {
+ my $content = $cache->thaw( $self->{post} );
+ if ($content) {
+ if ( $self->{developer_mode} ) {
+ say ' cache hit';
+ }
+ return $promise->resolve( ${$content} );
+ }
+ }
+
+ if ( $self->{developer_mode} ) {
+ say ' cache miss';
+ }
+
+ $self->{ua}->post_p( $url, $self->{post} )->then(
+ sub {
+ my ($tx) = @_;
+ if ( my $err = $tx->error ) {
+ $promise->reject(
+ "POST $url returned HTTP $err->{code} $err->{message}");
+ return;
+ }
+ my $content = $tx->res->body;
+ if ($cache) {
+ $cache->freeze( $self->{post}, \$content );
+ }
+ $promise->resolve($content);
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
- my $xp_err = XML::LibXML::XPathExpression->new('//Err');
- my $err = ( $self->{tree}->findnodes($xp_err) )[0];
+ return $promise;
+}
- if ($err) {
+sub check_mgate {
+ my ($self) = @_;
+
+ if ( $self->{raw_json}{err} and $self->{raw_json}{err} ne 'OK' ) {
+ $self->{errstr} = $self->{raw_json}{errTxt}
+ // 'error code is ' . $self->{raw_json}{err};
+ $self->{errcode} = $self->{raw_json}{err};
+ }
+ elsif ( defined $self->{raw_json}{cInfo}{code}
+ and $self->{raw_json}{cInfo}{code} ne 'OK'
+ and $self->{raw_json}{cInfo}{code} ne 'VH' )
+ {
+ $self->{errstr} = 'cInfo code is ' . $self->{raw_json}{cInfo}{code};
+ $self->{errcode} = $self->{raw_json}{cInfo}{code};
+ }
+ elsif ( @{ $self->{raw_json}{svcResL} // [] } == 0 ) {
+ $self->{errstr} = 'svcResL is empty';
+ }
+ elsif ( $self->{raw_json}{svcResL}[0]{err} ne 'OK' ) {
$self->{errstr}
- = $err->getAttribute('text')
- . ' (code '
- . $err->getAttribute('code') . ')';
- $self->{errcode} = $err->getAttribute('code');
+ = 'svcResL[0].err is ' . $self->{raw_json}{svcResL}[0]{err};
+ $self->{errcode} = $self->{raw_json}{svcResL}[0]{err};
}
- return;
+ return $self;
}
+sub add_message {
+ my ( $self, $json, $is_him ) = @_;
+
+ my $text = $json->{txtN};
+ my $code = $json->{code};
+
+ if ($is_him) {
+ $text = $json->{text};
+ $code = $json->{hid};
+ }
+
+ # Some backends use remL for operator information. We don't want that.
+ if ( $code eq 'OPERATOR' ) {
+ return;
+ }
+
+ for my $message ( @{ $self->{messages} } ) {
+ if ( $code eq $message->{code} and $text eq $message->{text} ) {
+ $message->{ref_count}++;
+ return $message;
+ }
+ }
+
+ my $message = Travel::Status::DE::HAFAS::Message->new(
+ json => $json,
+ is_him => $is_him,
+ ref_count => 1,
+ );
+ push( @{ $self->{messages} }, $message );
+ return $message;
+}
+
+sub parse_prodL {
+ my ($self) = @_;
+
+ my $common = $self->{raw_json}{svcResL}[0]{res}{common};
+ return [
+ map {
+ Travel::Status::DE::HAFAS::Product->new(
+ common => $common,
+ product => $_
+ )
+ } @{ $common->{prodL} }
+ ];
+}
+
+sub parse_search {
+ my ($self) = @_;
+
+ $self->{results} = [];
+
+ if ( $self->{errstr} ) {
+ return $self;
+ }
+
+ my @locL = @{ $self->{raw_json}{svcResL}[0]{res}{locL} // [] };
+
+ if ( $self->{raw_json}{svcResL}[0]{res}{match} ) {
+ @locL = @{ $self->{raw_json}{svcResL}[0]{res}{match}{locL} // [] };
+ }
+
+ @{ $self->{results} }
+ = map { Travel::Status::DE::HAFAS::Location->new( loc => $_ ) } @locL;
+
+ return $self;
+}
+
+sub parse_journey {
+ my ($self) = @_;
+
+ if ( $self->{errstr} ) {
+ return $self;
+ }
+
+ my $prodL = $self->parse_prodL;
+
+ my @locL = map { Travel::Status::DE::HAFAS::Location->new( loc => $_ ) }
+ @{ $self->{raw_json}{svcResL}[0]{res}{common}{locL} // [] };
+ my $journey = $self->{raw_json}{svcResL}[0]{res}{journey};
+ my @polyline;
+
+ my $poly = $journey->{poly};
+
+ # ÖBB
+ if ( $journey->{polyG} and @{ $journey->{polyG}{polyXL} // [] } ) {
+ $poly = $self->{raw_json}{svcResL}[0]{res}{common}{polyL}
+ [ $journey->{polyG}{polyXL}[0] ];
+ }
+
+ if ($poly) {
+ @polyline = decode_polyline( $poly->{crdEncYX} );
+ for my $ref ( @{ $poly->{ppLocRefL} // [] } ) {
+ my $poly = $polyline[ $ref->{ppIdx} ];
+ my $loc = $locL[ $ref->{locX} ];
+
+ $poly->{name} = $loc->name;
+ $poly->{eva} = $loc->eva;
+ }
+ }
+
+ $self->{result} = Travel::Status::DE::HAFAS::Journey->new(
+ common => $self->{raw_json}{svcResL}[0]{res}{common},
+ prodL => $prodL,
+ locL => \@locL,
+ journey => $journey,
+ polyline => \@polyline,
+ hafas => $self,
+ );
+
+ return $self;
+}
+
+sub parse_journey_match {
+ my ($self) = @_;
+
+ $self->{results} = [];
+
+ if ( $self->{errstr} ) {
+ return $self;
+ }
+
+ my $prodL = $self->parse_prodL;
+
+ my @locL = map { Travel::Status::DE::HAFAS::Location->new( loc => $_ ) }
+ @{ $self->{raw_json}{svcResL}[0]{res}{common}{locL} // [] };
+
+ my @jnyL = @{ $self->{raw_json}{svcResL}[0]{res}{jnyL} // [] };
+
+ for my $result (@jnyL) {
+ push(
+ @{ $self->{results} },
+ Travel::Status::DE::HAFAS::Journey->new(
+ common => $self->{raw_json}{svcResL}[0]{res}{common},
+ prodL => $prodL,
+ locL => \@locL,
+ journey => $result,
+ hafas => $self,
+ )
+ );
+ }
+ return $self;
+}
+
+sub parse_board {
+ my ($self) = @_;
+
+ $self->{results} = [];
+
+ if ( $self->{errstr} ) {
+ return $self;
+ }
+
+ my $prodL = $self->parse_prodL;
+
+ my @locL = map { Travel::Status::DE::HAFAS::Location->new( loc => $_ ) }
+ @{ $self->{raw_json}{svcResL}[0]{res}{common}{locL} // [] };
+ my @jnyL = @{ $self->{raw_json}{svcResL}[0]{res}{jnyL} // [] };
+
+ for my $result (@jnyL) {
+ eval {
+ push(
+ @{ $self->{results} },
+ Travel::Status::DE::HAFAS::Journey->new(
+ common => $self->{raw_json}{svcResL}[0]{res}{common},
+ prodL => $prodL,
+ locL => \@locL,
+ journey => $result,
+ hafas => $self,
+ )
+ );
+ };
+ if ($@) {
+ if ( $@ =~ m{Invalid local time for date in time zone} ) {
+
+ # Yes, HAFAS does in fact return invalid times during DST change
+ # (as in, it returns 02:XX:XX timestamps when the time jumps from 02:00:00 to 03:00:00)
+ # It's not clear what exactly is going wrong where and whether a 2:30 or a 3:30 journey is the correct one.
+ # For now, silently discard the affected journeys.
+ }
+ else {
+ warn("Skipping $result->{jid}: $@");
+ }
+ }
+ }
+ return $self;
+}
+
+# }}}
+# {{{ Public Functions
+
sub errcode {
my ($self) = @_;
@@ -259,9 +1057,6 @@ sub similar_stops {
if ( $service and exists $hafas_instance{$service}{stopfinder} ) {
- # we do not pass our constructor's language argument here,
- # because most stopfinder services do not return any results
- # for languages other than german ('d' aka the default)
my $sf = Travel::Status::DE::HAFAS::StopFinder->new(
url => $hafas_instance{$service}{stopfinder},
input => $self->{station},
@@ -277,91 +1072,94 @@ sub similar_stops {
return;
}
-sub results {
- my ($self) = @_;
- my $mode = $self->{post}->{boardType};
+sub similar_stops_p {
+ my ( $self, %opt ) = @_;
- my $xp_element = XML::LibXML::XPathExpression->new('//Journey');
- my $xp_msg = XML::LibXML::XPathExpression->new('./HIMMessage');
+ my $service = $self->{active_service};
- if ( defined $self->{results} ) {
- return @{ $self->{results} };
- }
- if ( not defined $self->{tree} ) {
- return;
+ 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},
+ input => $self->{station},
+ user_agent => $opt{user_agent},
+ developer_mode => $self->{developer_mode},
+ promise => $opt{promise},
+ );
}
+ return $opt{promise}
+ ->reject("stopfinder not available for backend '$service'");
+}
- $self->{results} = [];
-
- $self->{datetime_now} //= DateTime->now(
- time_zone => 'Europe/Berlin',
- );
- $self->{strptime_obj} //= DateTime::Format::Strptime->new(
- pattern => '%d.%m.%YT%H:%M',
- time_zone => 'Europe/Berlin',
- );
+sub station {
+ my ($self) = @_;
- for my $tr ( @{ $self->{tree}->findnodes($xp_element) } ) {
-
- my @message_nodes = $tr->findnodes($xp_msg);
- my $train = $tr->getAttribute('prod');
- my $time = $tr->getAttribute('fpTime');
- my $date = $tr->getAttribute('fpDate');
- my $dest = $tr->getAttribute('targetLoc');
- my $platform = $tr->getAttribute('platform');
- my $new_platform = $tr->getAttribute('newpl');
- my $delay = $tr->getAttribute('delay');
- my $e_delay = $tr->getAttribute('e_delay');
- my $info = $tr->getAttribute('delayReason');
- my @messages;
-
- if ( not( $time and $dest ) ) {
- next;
- }
+ if ( $self->{station_info} ) {
+ return $self->{station_info};
+ }
- for my $n (@message_nodes) {
- push( @messages, $n->getAttribute('header') );
- }
+ # no need to use Location instances here
+ my @locL = @{ $self->{raw_json}{svcResL}[0]{res}{common}{locL} // [] };
- substr( $date, 6, 0, '20' );
+ my %prefc_by_loc;
- # TODO the first charactor of delayReason is special:
- # " " -> no additional data, rest (if any) is delay reason
- # else -> first word is not a delay reason but additional data,
- # for instance "Zusatzfahrt/Ersatzfahrt" for a replacement train
- if ( defined $info and $info eq q{ } ) {
- $info = undef;
+ if ( $self->{active_service} and $self->{active_service} eq 'ÖBB' ) {
+ for my $jny ( @{ $self->{raw_json}{svcResL}[0]{res}{jnyL} // [] } ) {
+ if ( defined $jny->{stbStop}{locX} ) {
+ $prefc_by_loc{ $jny->{stbStop}{locX} } += 1;
+ }
}
- elsif ( defined $info and substr( $info, 0, 1 ) eq q{ } ) {
- substr( $info, 0, 1, q{} );
+ }
+ else {
+ for my $i ( 0 .. $#locL ) {
+ my $loc = $locL[$i];
+ if ( $loc->{pRefL} ) {
+ $prefc_by_loc{$i} = $#{ $loc->{pRefL} };
+ }
}
+ }
- $train =~ s{#.*$}{};
+ my @prefcounts = sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $prefc_by_loc{$_} ] } keys %prefc_by_loc;
- my $datetime = $self->{strptime_obj}->parse_datetime("${date}T${time}");
+ if ( not @prefcounts ) {
+ $self->{station_info} = {};
+ return $self->{station_info};
+ }
- push(
- @{ $self->{results} },
- Travel::Status::DE::HAFAS::Result->new(
- date => $date,
- datetime => $datetime,
- datetime_now => $self->{datetime_now},
- raw_delay => $delay,
- raw_e_delay => $e_delay,
- messages => \@messages,
- time => $time,
- train => $train,
- route_end => $dest,
- platform => $platform,
- new_platform => $new_platform,
- info => $info,
- )
- );
+ my $loc = $locL[ $prefcounts[0][0] ];
+
+ if ($loc) {
+ $self->{station_info} = {
+ name => $loc->{name},
+ eva => $loc->{extId},
+ names => [ map { $locL[ $_->[0] ]{name} } @prefcounts ],
+ evas => [ map { $locL[ $_->[0] ]{extId} } @prefcounts ],
+ };
+ }
+ else {
+ $self->{station_info} = {};
}
+ return $self->{station_info};
+}
+
+sub messages {
+ my ($self) = @_;
+ return @{ $self->{messages} };
+}
+
+sub results {
+ my ($self) = @_;
return @{ $self->{results} };
}
+sub result {
+ my ($self) = @_;
+ return $self->{result};
+}
+
# static
sub get_services {
my @services;
@@ -392,6 +1190,8 @@ sub get_active_service {
return;
}
+# }}}
+
1;
__END__
@@ -425,72 +1225,117 @@ monitors
=head1 VERSION
-version 2.03
+version 6.03
=head1 DESCRIPTION
Travel::Status::DE::HAFAS is an interface to HAFAS-based
-arrival/departure monitors, for instance the one available at
-L<http://reiseauskunft.bahn.de/bin/bhftafel.exe/dn>.
+arrival/departure monitors using the mgate.exe interface.
-It takes a station name and (optional) date and time and reports all arrivals
-or departures at that station starting at the specified point in time (now if
-unspecified).
+It can report departures/arrivals at a specific station, search for stations,
+or provide details about a specific journey. It supports non-blocking operation
+via promises.
=head1 METHODS
=over
-=item my $status = Travel::Status::DE::HAFAS->new(I<%opts>)
+=item my $status = Travel::Status::DE::HAFAS->new(I<%opt>)
-Requests the departures/arrivals as specified by I<opts> and returns a new
+Requests item(s) as specified by I<opt> and returns a new
Travel::Status::DE::HAFAS element with the results. Dies if the wrong
-I<opts> were passed.
+I<opt> were passed.
-Supported I<opts> are:
+I<opt> must contain either a B<station>, B<geoSearch>, B<locationSearch>, B<journey>, or B<journeyMatch> flag:
=over
=item B<station> => I<station>
-The station or stop to report for, e.g. "Essen HBf" or
-"Alfredusbad, Essen (Ruhr)". Mandatory.
+Request station board (arrivals or departures) for I<station>, e.g. "Essen HBf" or
+"Alfredusbad, Essen (Ruhr)". The station must be specified either by name or by
+EVA ID (e.g. 8000080 for Dortmund Hbf).
+Results are available via C<< $status->results >>.
+
+=item B<geoSearch> => B<{> B<lat> => I<latitude>, B<lon> => I<longitude> B<}>
-=item B<date> => I<dd>.I<mm>.I<yyyy>
+Search for stations near I<latitude>, I<longitude>.
+Results are available via C<< $status->results >>.
-Date to report for. Defaults to the current day.
+=item B<locationSearch> => I<query>
+
+Search for stations whose name is similar to I<query>.
+Results are available via C<< $status->results >>.
+
+=item B<journey> => B<{> B<id> => I<tripid> [, B<name> => I<line> ] B<}>
+
+Request details about the journey identified by I<tripid> and I<line>.
+The result is available via C<< $status->result >>.
+
+=item B<journeyMatch> => I<query>
+
+Request journeys that match I<query> (e.g. "ICE 205" or "S 31111").
+Results are available via C<< $status->results >>.
+In contrast to B<journey>, the results typically only contain a minimal amount
+of information: trip ID, train/line identifier, and first and last stop. There
+is no real-time data.
+
+=back
+
+The following optional flags may be set.
+Values in brackets indicate flags that are only relevant in certain request
+modes, e.g. geoSearch or journey.
+
+=over
-=item B<excluded_mots> => [I<mot1>, I<mot2>, ...]
+=item B<arrivals> => I<bool> (station)
+
+Request arrivals (if I<bool> is true) rather than departures (if I<bool> is
+false or B<arrivals> is not specified).
+
+=item B<cache> => I<Cache::File object>
+
+Store HAFAS replies in the provided cache object. This module works with
+real-time data, so the object should be configured for an expiry of one to two
+minutes.
+
+=item B<datetime> => I<DateTime object> (station)
+
+Date and time to report for. Defaults to now.
+
+=item B<excluded_mots> => [I<mot1>, I<mot2>, ...] (geoSearch, station, journeyMatch)
By default, all modes of transport (trains, trams, buses etc.) are returned.
If this option is set, all modes appearing in I<mot1>, I<mot2>, ... will
be excluded. The supported modes depend on B<service>, use
B<get_services> or B<get_service> to get the supported values.
-Note that this parameter does not work if the B<url> parameter is set.
-
-=item B<exclusive_mots> => [I<mot1>, I<mot2>, ...]
+=item B<exclusive_mots> => [I<mot1>, I<mot2>, ...] (geoSearch, station, journeyMatch)
If this option is set, only the modes of transport appearing in I<mot1>,
I<mot2>, ... will be returned. The supported modes depend on B<service>, use
B<get_services> or B<get_service> to get the supported values.
-Note that this parameter does not work if the B<url> parameter is set.
-
=item B<language> => I<language>
-Set language for additional information. Accepted arguments are B<d>eutsch,
-B<e>nglish, B<i>talian and B<n> (dutch), depending on the used service.
+Request text messages to be provided in I<language>. Supported languages depend
+on B<service>, use B<get_services> or B<get_service> to get the supported
+values. Providing an unsupported or invalid value may lead to garbage output.
+
+=item B<lookahead> => I<int> (station)
+
+Request arrivals/departures that occur up to I<int> minutes after the specified datetime.
+Default: -1 (do not limit results by time).
=item B<lwp_options> => I<\%hashref>
Passed on to C<< LWP::UserAgent->new >>. Defaults to C<< { timeout => 10 } >>,
-you can use an empty hashref to override it.
+pass an empty hashref to call the LWP::UserAgent constructor without arguments.
-=item B<mode> => B<arr>|B<dep>
+=item B<results> => I<count> (geoSearch, locationSearch, station)
-By default, Travel::Status::DE::HAFAS reports train departures
-(B<dep>). Set this to B<arr> to get arrivals instead.
+Request up to I<count> results.
+Default: 30.
=item B<service> => I<service>
@@ -498,13 +1343,33 @@ Request results from I<service>, defaults to "DB".
See B<get_services> (and C<< hafas-m --list >>) for a list of supported
services.
-=item B<time> => I<hh>:I<mm>
+=item B<with_polyline> => I<bool> (journey)
-Time to report for. Defaults to now.
+Request a polyline (series of geo-coordinates) indicating the train's route.
-=item B<url> => I<url>
+=back
+
+=item my $status_p = Travel::Status::DE::HAFAS->new_p(I<%opt>)
+
+Returns a promise that resolves into a Travel::Status::DE::HAFAS instance
+($status) on success and rejects with an error message on failure. If the
+failure occured after receiving a response from the HAFAS backend, the rejected
+promise contains a Travel::Status::DE::HAFAS instance as a second argument.
+This instance can be used e.g. to call similar_stops_p in case of an ambiguous
+location specifier. In addition to the arguments of B<new>, the following
+mandatory arguments must be set.
+
+=over
-Request results from I<url>, defaults to the one belonging to B<service>.
+=item B<promise> => I<promises module>
+
+Promises implementation to use for internal promises as well as B<new_p> return
+value. Recommended: Mojo::Promise(3pm).
+
+=item B<user_agent> => I<user agent>
+
+User agent instance to use for asynchronous requests. The object must implement
+a B<post_p> function. Recommended: Mojo::UserAgent(3pm).
=back
@@ -518,39 +1383,119 @@ as string. If no backend error occurred, returns undef.
In case of an error in the HTTP request or HAFAS backend, returns a string
describing it. If no error occurred, returns undef.
-=item $status->results
+=item $status->results (geoSearch, locationSearch)
+
+Returns a list of stop locations. Each list element is a
+Travel::Status::DE::HAFAS::Location(3pm) object.
+
+If no matching results were found or the parser / http request failed, returns
+an empty list.
+
+=item $status->results (station)
Returns a list of arrivals/departures. Each list element is a
-Travel::Status::DE::HAFAS::Result(3pm) object.
+Travel::Status::DE::HAFAS::Journey(3pm) object.
If no matching results were found or the parser / http request failed, returns
undef.
+=item $status->results (journeyMatch)
+
+Returns a list of Travel::Status::DE::HAFAS::Journey(3pm) object that describe
+matching journeys. In general, these objects lack real-time data,
+intermediate stops, and more.
+
+=item $status->result (journey)
+
+Returns a single Travel::Status::DE::HAFAS::Journey(3pm) object that describes
+the requested journey.
+
+If no result was found or the parser / http request failed, returns undef.
+
+=item $status->messages
+
+Returns a list of Travel::Status::DE::HAFAS::Message(3pm) objects with service
+messages. Each message belongs to at least one arrival/departure (station,
+journey) or to at least stop alongside its route (journey).
+
+=item $status->station
+
+Returns a hashref describing the departure stations in all requested journeys.
+The hashref contains four entries: B<names> (station names), B<name> (most
+common name), B<evas> (UIC / EVA IDs), and B<eva> (most common UIC / EVA ID).
+These are subject to change.
+
+Note that the most common name and ID may be different from the station for
+which departures were requested, as HAFAS uses different identifiers for train
+stations, bus stops, and other modes of transit even if they are interlinked.
+
+Not available in journey mode.
+
=item $status->similar_stops
Returns a list of hashrefs describing stops whose name is similar to the one
requested in the constructor's B<station> parameter. Returns nothing if
the active service does not support this feature.
-This is most useful if B<errcode> returns 'H730', which means that the
+This is most useful if B<errcode> returns 'LOCATION', which means that the
HAFAS backend could not identify the stop.
See Travel::Status::DE::HAFAS::StopFinder(3pm)'s B<results> method for details
on the return value.
+=item $status->similar_stops_p(I<%opt>)
+
+Returns a promise resolving to a list of hashrefs describing stops whose name
+is similar to the one requested in the constructor's B<station> parameter.
+Returns nothing if the active service does not support this feature. This is
+most useful if B<errcode> returns 'LOCATION', which means that the HAFAS
+backend could not identify the stop.
+
+See Travel::Status::DE::HAFAS::StopFinder(3pm)'s B<results> method for details
+on the resolved values.
+
+If $status has been created using B<new_p>, this function does not require
+arguments. Otherwise, the caller must specify B<promise> and B<user_agent>
+(see B<new_p> above).
+
=item $status->get_active_service
Returns a hashref describing the active service when a service is active and
-nothing otherwise. The hashref contains the keys B<url> (URL to the station
-board service), B<stopfinder> (URL to the stopfinder service, if supported),
-B<name>, and B<productbits> (arrayref describing the supported modes of
-transport, may contain duplicates).
+nothing otherwise. The hashref contains the following keys.
+
+=over
+
+=item B<name> => I<string>
+
+service name, e.g. Bay Area Rapid Transit or Deutsche Bahn.
+
+=item B<mgate> => I<string>
+
+HAFAS backend URL
+
+=item B<languages> => I<arrayref>
+
+Languages supported by the backend; see the constructor's B<language> argument.
+
+=item B<productbits> => I<arrayref>
+
+MOT bits supported by the backend. I<arrayref> contains either strings
+(one string per mode of transit) or arrayrefs (one string pair per mode of
+transit, with the first entry referring to the MOT identifier and the second
+one containing a slightly longer description of it).
+
+=item B<time_zone> => I<string> (optional)
+
+The time zone this service reports arrival/departure times in. If this key is
+not present, it is safe to assume that it uses Europe/Berlin.
+
+=back
=item Travel::Status::DE::HAFAS::get_services()
Returns an array containing all supported HAFAS services. Each element is a
hashref and contains all keys mentioned in B<get_active_service>.
It also contains a B<shortname> key, which is the service name used by
-the constructor's B<service> parameter.
+the constructor's B<service> parameter, e.g. BART or DB.
=item Travel::Status::DE::HAFAS::get_service(I<$service>)
@@ -575,8 +1520,6 @@ None.
=item * LWP::UserAgent(3pm)
-=item * XML::LibXML(3pm)
-
=back
=head1 BUGS AND LIMITATIONS
@@ -585,11 +1528,18 @@ The non-default services (anything other than DB) are not well tested.
=head1 SEE ALSO
-Travel::Status::DE::HAFAS::Result(3pm), Travel::Status::DE::HAFAS::StopFinder(3pm).
+=over
+
+=item * L<https://dbf.finalrewind.org?hafas=1> provides a web frontend to most
+of this module's features. Set B<hafas=>I<service> to use a specific service.
+
+=item * Travel::Routing::DE::HAFAS(3pm) for itineraries.
+
+=back
=head1 AUTHOR
-Copyright (C) 2015-2017 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt>
+Copyright (C) 2015-2024 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt>
=head1 LICENSE