diff options
38 files changed, 5145 insertions, 757 deletions
diff --git a/.github/workflows/perl.yml b/.github/workflows/perl.yml new file mode 100644 index 0000000..792826f --- /dev/null +++ b/.github/workflows/perl.yml @@ -0,0 +1,33 @@ +name: linux + +on: + push: + branches: + - '*' + pull_request: + branches: + - '*' + +jobs: + perl: + + runs-on: ubuntu-latest + + strategy: + matrix: + perl-version: + - '5.20' + - 'latest' + - 'threaded' + + container: + image: perl:${{ matrix.perl-version }} + + steps: + - uses: actions/checkout@v2 + - name: perl -V + run: perl -V + - name: Install Dependencies + run: curl -sL https://raw.githubusercontent.com/skaji/cpm/master/cpm | perl - install -g --show-build-log-on-failure + - name: Run Tests + run: prove -l t diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000..c69b0d4 --- /dev/null +++ b/.mailmap @@ -0,0 +1 @@ +Birte Kristina Friesel <derf@finalrewind.org> diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index c8181b7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: perl -perl: - - "5.26" - - "5.24" - - "5.22" - - "5.20" - - "5.18" - - "5.16" - - "5.14" @@ -17,24 +17,26 @@ Module::Build->new( module_name => 'Travel::Status::DE::DeutscheBahn', license => 'perl', requires => { - 'perl' => '5.14.0', - 'Carp' => 0, - 'Class::Accessor' => '0.16', - 'DateTime' => 0, + 'perl' => '5.14.0', + 'Carp' => 0, + 'Class::Accessor' => '0.16', + 'DateTime' => 0, 'DateTime::Format::Strptime' => 0, - 'Getopt::Long' => 0, - 'JSON' => 0, - 'List::MoreUtils' => 0, - 'List::Util' => 0, - 'LWP::UserAgent' => 0, - 'POSIX' => 0, - 'XML::LibXML' => '1.70', + 'Digest::MD5' => 0, + 'Getopt::Long' => 0, + 'JSON' => 0, + 'List::MoreUtils' => 0, + 'List::Util' => 0, + 'LWP::UserAgent' => 0, + 'LWP::Protocol::https' => 0, }, - script_files => 'bin/', - sign => 1, - meta_merge => { + script_files => 'bin/', + sign => 1, + test_requires => { 'File::Slurp' => 0 }, + meta_merge => { resources => { - repository => 'https://github.com/derf/Travel-Status-DE-DeutscheBahn' + repository => + 'https://github.com/derf/Travel-Status-DE-DeutscheBahn' } }, )->create_build_script(); @@ -1,4 +1,4 @@ -Copyright (C) 2015 by Daniel Friesel <derf@finalrewind.org> +Copyright (C) 2015-2023 by Birte Kristina Friesel <derf@finalrewind.org> All files in this distribution are licensed under the same terms as Perl itself. @@ -1,3 +1,249 @@ +Travel::Status::DE::DeutscheBahn 6.03 - Mon Apr 15 2024 + + * Journey: Add product_at accessor + +Travel::Status::DE::DeutscheBahn 6.02 - Fri Apr 12 2024 + + * hafas-m: Add -j / --with-jid option + * $hafas->station: Correctly determine "name" and "eva" for stations that + have multiple EVA IDs and names. + +Travel::Status::DE::DeutscheBahn 6.01 - Thu Apr 04 2024 + + * Journey: Add operators accessor + * Add CMTA (Capital Metropolitan Transportation Authority) and BLS (BLS AG) + services + +Travel::Status::DE::DeutscheBahn 6.00 - Tue Apr 02 2024 + + * Add AVV (Aachener Verkehrsverbund), BART (Bay Area Rapid Transit), and IE + (Iarnród Éireann / Irish Rail) backends + * Support backend-specific time zones; add time_zone to service description + * Handle cross-timezone journeys and stops whose time zone differs from + the backend's default time zone. As of this release, all input and output + datetimes refer to the backend's default time zone rather than local + time (Stop and Journey accessors) / whatever (hafas-m and HAFAS + arguments). Use the new tz_offset accessor to determine local time. + This is a breaking change. + * Journey, Stop: Add tz_offset accessor + +Travel::Status::DE::DeutscheBahn 5.06 - Thu Mar 28 2024 + + * Add Travel::Status::DE::HAFAS::Product module to handle line numbers, + operators, and similar. This enables proper support for journeys with + multiple operators and possibly multiple train/line numbers along the + route. It also exposes the line ID. + * Journey: Add product accessor + * Stop: Add prod_arr, prod_dep accessors + * hafas-m: Improve stop list display in journey mode + * Fix polyline and platform number support in ÖBB backend (and possibly + other non-DB backends) + * ÖBB backend: correctly handle polylines and platform numbers + * ÖBB backend: Remove redundant train numbers from $journey->name / + $journey->line / $product->name + * ÖBB backend: update productbits (thanks to Cassidy Dingenskirchen) + +Travel::Status::DE::DeutscheBahn 5.05 - Wed Feb 21 2024 + + * ÖBB backend: handle midnight crossing and fix associated warnings + (patch by Cassidy Dingenskirchen) + * ÖBB backend: adjust request version to fix backend errors with certain + trains (patch by Cassidy Dingenskirchen) + +Travel::Status::DE::DeutscheBahn 5.04 - Sat Dec 30 2023 + + * Journey: Add is_additional accessor + * Stop: Add is_additional accessor + +Travel::Status::DE::DeutscheBahn 5.03 - Wed Dec 19 2023 + + * Journey: Fix route_interesting accessor (broken by 5.00) + +Travel::Status::DE::DeutscheBahn 5.02 - Wed Dec 14 2023 + + * Fix calls to $journey->route crashing in handle_day_change or add_message + in some circumstances if the $hafas object that created $journey has + gone out of scope (and become eligible for garbage collection) in the + meantime. The culprit was a mis-application of weaken() introduced in + v5.01. + * hafas-m: Add -v/--via option + +Travel::Status::DE::DeutscheBahn 5.01 - Sat Nov 25 2023 + + * Fix incorrect handling of HIM messages (introduced in 4.19) + * hafas-m: Declutter station board output: sort by real-time data and show + all messages at the end + * Journey: Support construction from JSON that does not provide a date + attribute (i.e., add date parameter to constructor) + * Stop: Add messages accessor + +Travel::Status::DE::DeutscheBahn 5.00 - Tue Nov 21 2023 + + * hafas-m: Add --raw-json option (useful for HAFAS client development) + * hafas-m: Add train search support, e.g. "hafas-m '!EC 6'" + * HAFAS->new, HAFAS->new_p: Add journeyMatch flag + * Add Travel::Status::DE::HAFAS::Location module + * HAFAS::Stop: Fix some documented accessors not being provided by the + module + * HAFAS::Stop: Add "loc" accessor that returns the corresponding Location + instance + * HAFAS::Stop: Breaking change: The "eva", "name", "lat", "lon", + "distance_m", and "weight" accessors are no longer supported. Use the + (identically named) Location accessors instead, i.e., "$stop->loc->name" + instead of "$stop->name", etc. + +Travel::Status::DE::DeutscheBahn 4.19 - Sat Nov 11 2023 + + * hafas-m: Fall back to locationSearch if StopFinder is unavailable + * Message: Expose message type; document ->code and ->type + +Travel::Status::DE::DeutscheBahn 4.18 - Sun Oct 29 2023 + + * Allow HAFAS and hafas-m users to specify the backend language + * Update services list to include known language specifiers + * hafas-m: Add -l / --language option + * HAFAS->new, ->new_p: Add language option + +Travel::Status::DE::DeutscheBahn 4.17 - Sun Sep 03 2023 + + * Journey: handle empty / undefined nameS fields + +Travel::Status::DE::DeutscheBahn 4.16 - Sun Sep 03 2023 + + * Journey: Fix date parser for cross-midnight journeys that start on the + 1st through 9th day of a month. + +Travel::Status::DE::DeutscheBahn 4.15 - Wed Aug 22 2023 + + * HAFAS: Add similar_stops_p function + +Travel::Status::DE::DeutscheBahn 4.14 - Tue Aug 22 2023 + + * Journey: Correctly calculate datetimes when requesting a cross-midnight + journey after midnight. Previously, those were off by 24 hours. + +Travel::Status::DE::DeutscheBahn 4.13 - Mon Aug 21 2023 + + * new_p: Return a Travel::Status::DE::HAFAS instance in addition to the + error message when rejecting a promise after receiving a HAFAS reply + +Travel::Status::DE::DeutscheBahn 4.12 - Mon May 29 2023 + + * Fix Journey->route and HAFAS geoSearch / locationSearch returning + incorrect geocoordinates (latitude and longitude were mixed up) + +Travel::Status::DE::DeutscheBahn 4.11 - Thu May 25 2023 + + * hafas-m: Add --json option + +Travel::Status::DE::DeutscheBahn 4.10 - Tue Apr 18 2023 + + * HAFAS->new, ->new_p: Add locationSearch option (search stops by name) + * Journey->route, ->route_interesting: + Return Travel::Status::DE::HAFAS::Stop instances + +Travel::Status::DE::DeutscheBahn 4.09 - Mon Apr 10 2023 + + * HAFAS->new: Add geoSearch option (search stops by coordinates) + +Travel::Status::DE::DeutscheBahn 4.08 - Sun Feb 12 2023 + + * Journey: Correctly report route_end / origin in arrivals mode + * Journey->route: Add platform, sched_platform, rt_platform, and + is_changed_platform accessors + +Travel::Status::DE::DeutscheBahn 4.07 - Sun Feb 05 2023 + + * hafas-m: Fix uninitialized value warnings in "--list" output + * Improve support for non-DB HAFAS instances + * Fix day change handling in departure board mode. + Previously, journeys arriving / departing after midnight had wrong + timestamps in some cases. + +Travel::Status::DE::DeutscheBahn 4.06 - Sat Feb 04 2023 + + * HAFAS->station: rename "uic" to "eva"; add "names" and "evas" keys + * Rename Journey->uic to Journey->eva + +Travel::Status::DE::DeutscheBahn 4.05 - Fri Feb 03 2023 + + * StopFinder: add new_p constructor for async requests via promises + +Travel::Status::DE::DeutscheBahn 4.04 - Mon Jan 30 2023 + + * Journey->is_cancelled: correctly report cancellations in station board + mode + +Travel::Status::DE::DeutscheBahn 4.03 - Sat Jan 28 2023 + + * HAFAS: Add "station" accessor + * Journey: Add "station", "station_uic" and "line_no" accessors + * Journey->line now returns journey type as well as line number + * Journey->line_no provides the old Journey->line behaviour + * Journey: Add "route_interesting" accessor + +Travel::Status::DE::DeutscheBahn 4.02 - Sun Nov 06 2022 + + * HAFAS->new: Add "results" and "lookahead" options + * Build.PL: Specify File::Slurp test dependency + +Travel::Status::DE::DeutscheBahn 4.01 - Sat Oct 29 2022 + + * Journey: Add "class" accessor + * Message: Correctly document "short" and "text" accessors + ("header" and "lead" were not supported), add "is_him" accessor + * Specify List::MoreUtils and List::Util dependencies for bin/hafas-m + +Travel::Status::DE::DeutscheBahn 4.00 - Fri Oct 28 2022 + + * Use mgate.exe HAFAS interface instead of stboard.exe/bhftafel.exe. + This introduces several breaking changes in hafas-m, + Travel::Status::DE::HAFAS, and Travel::StatuS::DE::HAFAS::Result. + * hafas-m: Options -l/--lang, -L/--ignore-late, and -u/--url are no longer + supported + * hafas-m now supports journey details by specifying a journey ID instead + of a station name. + * Travel::Status::DE::HAFAS->new: "date" and "time" keys are no longer + supported. Use "datetime" instead. + * Travel::Status::DE::HAFAS->new: "lang" key is no longer supported. + * Travel::Status::DE::HAFAS->new: "url" key is no longer supported. + * Travel::Status::DE::HAFAS->new: "mode" key is no longer supported. Set + "arrivals" to a true value to request arrivals instead of departures. + * Travel::Status::DE::HAFAS->new: add optional "cache" key and support for + "journey" requests with optional "with_polyline" key. + * Travel::Status::DE::HAFAS: add new_p constructor for async requests via + Promises. + * Travel::Status::DE::HAFAS: Add "result" and "messages" accessors. + * Rename Travel::Status::DE::HAFAS::Result to ...::Journey. The accessors + "sched_date", "date", "info", "countdown", "countdown_sec", + "raw_e_delay", "raw_delay", "sched_time", "time", "train", "train_no" + and "line_no" are no longer supported. Introduces several new ones + instead. + * The module no longer depends on XML::LibXML + * New dependency: Digest::MD5 + +Travel::Status::DE::DeutscheBahn 3.01 - Sat Jun 06 2020 + + * Fix support for ÖBB and other backends which recently switched from + two- to four-digit years + +Travel::Status::DE::DeutscheBahn 3.00 - Sat May 09 2020 + + * Result: The date, datetime, and time accessors now report realtime + data if available + * Result: Add sched_date, sched_datetime, and sched_time accessors for + schedule data + +Travel::Status::DE::DeutscheBahn 2.05 - Sun May 03 2020 + + * Document LWP::Protocol::https dependency + * Remove BVG (Berliner Verkehrsgesellschaft) and SBB + (Schweizerische Bundesbahnen) services + +Travel::Status::DE::DeutscheBahn 2.04 - Sun Dec 30 2018 + + * Handle invalid XML when using the VBB backend + Travel::Status::DE::DeutscheBahn 2.03 - Sat Dec 16 2017 * Update DB backend API URL diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..334951d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM perl:5.30-slim + +COPY bin/ /app/bin/ +COPY lib/ /app/lib/ +COPY Build.PL cpanfile* /app/ + +WORKDIR /app + +ARG DEBIAN_FRONTEND=noninteractive +ARG APT_LISTCHANGES_FRONTEND=none + +RUN apt-get update \ + && apt-get -y --no-install-recommends install ca-certificates curl gcc libc6-dev libssl1.1 libssl-dev make zlib1g-dev \ + && cpanm -n --no-man-pages --installdeps . \ + && perl Build.PL \ + && perl Build \ + && rm -rf ~/.cpanm \ + && apt-get -y purge curl gcc libc6-dev libssl-dev make zlib1g-dev \ + && apt-get -y autoremove \ + && apt-get -y clean \ + && rm -rf /var/cache/apt/* /var/lib/apt/lists/* + +ENTRYPOINT ["perl", "-Ilib", "bin/hafas-m"] @@ -1,44 +0,0 @@ -Travel::Status::DE::DeutscheBahn - -Interface to the DeutscheBahn online departure monitor ------------------------------------------------------- - -* <http://finalrewind.org/projects/Travel-Status-DE-DeutscheBahn/> - - -Dependencies ------------- - - * perl version 5.10.1 or newer - * Class::Accessor - * DateTime - * DateTime::Format::Strptime - * JSON - * List::MoreUtils - * LWP::UserAgent (usually shipped by libwww-perl) - * XML::LibXML - -Installation ------------- - -From a release tarball: - -$ perl Build.PL -$ perl Build -$ sudo perl Build install - -From git: - -$ perl Build.PL -$ perl Build -$ perl Build manifest -$ sudo perl Build install - -You can then run 'man Travel::Status::DE::HAFAS' for more information. -This distribution also ships the example script 'hafas-m', see 'man hafas-m'. - -Testing -------- - -$ perl Build test - -This requires the Test::More, Test::Compile and Test::Pod modules. diff --git a/README.md b/README.md new file mode 100644 index 0000000..25d97bf --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +# hafas-m - Commandline Public Transit Departure Monitor + +**hafas-m** is a commandline client and Perl module for HAFAS public transit +departure interfaces. It supports a variety of transit services in Europe and +parts of North America, with a special focus on the ones operated by +Deutsche Bahn (DB) and Österreichische Bundesbahnen (ÖBB). + +This README documents installation of hafas-m and the associated +Travel::Status::DE::HAFAS Perl module. See the [Travel::Status::DE::HAFAS +homepage](https://finalrewind.org/projects/Travel-Status-DE-DeutscheBahn) and +[hafas-m manual](https://man.finalrewind.org/1/hafas-m) for a feature overview +and usage instructions. A web frontend to Travel::Status::DE::HAFAS is +available at [dbf.finalrewind.org](https://dbf.finalrewind.org/?hafas=DB). + +## Installation + +You have five installation options: + +* `.deb` releases for Debian-based distributions +* finalrewind.org APT repository for Debian-based distributions +* Installing the latest release from CPAN +* Installation from source +* Using a Docker image + +Except for Docker, __hafas-m__ is available in your PATH after installation. +You can run `hafas-m --version` to verify this. Documentation is available via +`man hafas-m`. + +### Release Builds for Debian + +[lib.finalrewind.org/deb](https://lib.finalrewind.org/deb) provides Debian +packages of all release versions. Note that these are not part of the official +Debian repository and are not covered by its quality assurance process. + +To install the latest release, run: + +``` +wget https://lib.finalrewind.org/deb/libtravel-status-de-deutschebahn-perl_latest_all.deb +sudo apt install ./libtravel-status-de-deutschebahn-perl_latest_all.deb +rm libtravel-status-de-deutschebahn-perl_latest_all.deb +``` + +Uninstallation works as usual: + +``` +sudo apt remove libtravel-status-de-deutschebahn-perl +``` + +### finalrewind.org APT repository + +[lib.finalrewind.org/apt](https://lib.finalrewind.org/apt) provides an APT +repository with Debian packages of the latest release versions. Note that this +is not a Debian repository; it is operated under a best-effort SLA and if you +use it you will have to trust me not to screw up your system with bogus +packages. Also, note that the packages are not part of the official Debian +repository and are not covered by its quality assurance process. + +To set up the repository and install the latest Travel::Status::DE::DeutscheBahn +release, run: + +``` +curl -s https://finalrewind.org/apt.asc | sudo tee /etc/apt/trusted.gpg.d/finalrewind.asc +echo 'deb https://lib.finalrewind.org/apt stable main' | sudo tee /etc/apt/sources.list.d/finalrewind.list +sudo apt update +sudo apt install libtravel-status-de-deutschebahn-perl +``` + +Afterwards, `apt update` and `apt upgrade` will automatically install new +Travel::Status::DE::DeutscheBahn releases. + +Uninstallation of Travel::Status::DE::DeutscheBahn works as usual: + +``` +sudo apt remove libtravel-status-de-deutschebahn-perl +``` + +To remove the APT repository from your system, run: + +``` +sudo rm /etc/apt/trusted.gpg.d/finalrewind.asc \ + /etc/apt/sources.list.d/finalrewind.list +``` + +### Installation from CPAN + +Travel::Status::DE::DeutscheBahn releases are published on the Comprehensive +Perl Archive Network (CPAN) and can be installed using standard Perl module +tools such as `cpanminus`. + +Before proceeding, ensure that you have standard build tools (i.e. make, +pkg-config and a C compiler) installed. You will also need the following +libraries with development headers: + +* libssl +* zlib + +Now, use a tool of your choice to install the module. Minimum working example: + +``` +cpanm Travel::Status::DE::DeutscheBahn +``` + +If you run this as root, it will install script and module to `/usr/local` by +default. There is no well-defined uninstallation procedure. + +### Installation from Source + +In this variant, you must ensure availability of dependencies by yourself. +You may use carton or cpanminus with the provided `Build.PL`, Module::Build's +installdeps command, or rely on the Perl modules packaged by your distribution. +On Debian 10+, all dependencies are available from the package repository. + +To check whether dependencies are satisfied, run: + +``` +perl Build.PL +``` + +If it complains about "... is not installed" or "ERRORS/WARNINGS FOUND IN +PREREQUISITES", it is missing dependencies. + +Once all dependencies are satisfied, use Module::Build to build, test and +install the module. Testing is optional -- you may skip the "Build test" +step if you like. + +If you downloaded a release tarball, proceed as follows: + +``` +./Build +./Build test +sudo ./Build install +``` + +If you are using the Git repository, use the following commands: + +``` +./Build +./Build manifest +./Build test +sudo ./Build install +``` + +Note that system-wide installation does not have a well-defined uninstallation +procedure. + +If you do not have superuser rights or do not want to perform a system-wide +installation, you may leave out `Build install` and use **hafas-m** from the +current working directory. + +With carton: + +``` +carton exec hafas-m --version +``` + +Otherwise (also works with carton): + +``` +perl -Ilocal/lib/perl5 -Ilib bin/hafas-m --version +``` + +### Running hafas-m via Docker + +A hafas-m image is available on Docker Hub. It is intended for testing +purposes: due to the latencies involved in spawning a container for each +hafas-m invocation, it is less convenient for day-to-day usage. + +Installation: + +``` +docker pull derfnull/hafas-m:latest +``` + +Use it by prefixing hafas-m commands with `docker run --rm +derfnull/hafas-m:latest`, like so: + +``` +docker run --rm derfnull/hafas-m:latest --version +``` + +Documentation is not available in this image. Please refer to the +[online hafas-m manual](https://man.finalrewind.org/1/hafas-m/) instead. diff --git a/bin/hafas-m b/bin/hafas-m index b2efd2b..ea8471f 100755 --- a/bin/hafas-m +++ b/bin/hafas-m @@ -1,23 +1,27 @@ -#!/usr/bin/env perl +#!perl use strict; use warnings; use 5.014; -our $VERSION = '2.03'; +our $VERSION = '6.03'; +use utf8; +use DateTime; use Encode qw(decode); -use Getopt::Long qw(:config no_ignore_case); +use JSON; +use Getopt::Long qw(:config no_ignore_case); use List::MoreUtils qw(uniq); -use List::Util qw(first max); +use List::Util qw(first max); use Travel::Status::DE::HAFAS; -my ( $date, $time ); -my $arrivals = 0; -my $ignore_late = 0; -my $types = q{}; -my $language; +my ( $date, $time, $language ); +my $arrivals; +my $show_jid; +my $types = q{}; my $developer_mode; -my ( $list_services, $service, $hafas_url ); +my $via; +my ( $json_output, $raw_json_output ); +my ( $list_services, $service ); my ( @excluded_mots, @exclusive_mots ); my @output; @@ -27,45 +31,116 @@ for my $arg (@ARGV) { $arg = decode( 'UTF-8', $arg ); } +my $output_bold = -t STDOUT ? "\033[1m" : q{}; +my $output_reset = -t STDOUT ? "\033[0m" : q{}; + GetOptions( - 'a|arrivals' => \$arrivals, - 'd|date=s' => \$date, - 'h|help' => sub { show_help(0) }, - 'l|lang=s' => \$language, - 'L|ignore-late' => \$ignore_late, - 'm|mot=s' => \$types, - 's|service=s' => \$service, - 't|time=s' => \$time, - 'u|url=s' => \$hafas_url, - 'V|version' => \&show_version, - 'devmode' => \$developer_mode, - 'list' => \$list_services, + 'a|arrivals' => \$arrivals, + 'd|date=s' => \$date, + 'h|help' => sub { show_help(0) }, + 'j|with-jid' => \$show_jid, + 'l|language=s' => \$language, + 'm|mot=s' => \$types, + 's|service=s' => \$service, + 't|time=s' => \$time, + 'v|via=s' => \$via, + 'V|version' => \&show_version, + 'devmode' => \$developer_mode, + 'json' => \$json_output, + 'raw-json' => \$raw_json_output, + 'list' => \$list_services, ) or show_help(1); if ($list_services) { - printf( "%-40s %-14s %s\n\n", 'operator', 'abbr. (-s)', 'url (-u)' ); + printf( + "%-40s %-14s %-15s %s\n\n", + 'operator', 'abbr. (-s)', 'languages (-l)', + 'time zone' + ); for my $service ( Travel::Status::DE::HAFAS::get_services() ) { - printf( "%-40s %-14s %s\n", @{$service}{qw(name shortname url)} ); + printf( + "%-40s %-14s %-15s %s\n", + @{$service}{qw(name shortname)}, + join( q{ }, @{ $service->{languages} // [] } ), + $service->{time_zone} // q{}, + ); } exit 0; } parse_mot_options(); -my $status = Travel::Status::DE::HAFAS->new( - date => $date, - language => $language, +my %opt = ( excluded_mots => \@excluded_mots, exclusive_mots => \@exclusive_mots, station => shift || show_help(1), - time => $time, - mode => $arrivals ? 'arr' : 'dep', + arrivals => $arrivals, developer_mode => $developer_mode, service => $service, - url => $hafas_url, + language => $language, ); +if ( $opt{station} =~ m{ ^ (?<lat> [0-9.]+ ) : (?<lon> [0-9].+ ) $ }x ) { + $opt{geoSearch} = { + lat => $+{lat}, + lon => $+{lon}, + }; + delete $opt{station}; +} +elsif ( $opt{station} =~ m{ ^ [?] (?<query> .*) $ }x ) { + $opt{locationSearch} = $+{query}; + delete $opt{station}; +} +elsif ( $opt{station} =~ m{[|]} ) { + $opt{journey} = { id => $opt{station} }; + delete $opt{station}; +} +elsif ( $opt{station} =~ m{ ^ [!] (?<query> .*) $ }x ) { + $opt{journeyMatch} = $+{query}; + delete $opt{station}; +} + +if ( $date or $time ) { + my $desc = Travel::Status::DE::HAFAS::get_service($service) // {}; + my $dt + = DateTime->now( time_zone => $desc->{time_zone} // 'Europe/Berlin' ); + if ($date) { + if ( $date + =~ m{ ^ (?<day> \d{1,2} ) [.] (?<month> \d{1,2} ) [.] (?<year> \d{4})? $ }x + ) + { + $dt->set( + day => $+{day}, + month => $+{month} + ); + if ( $+{year} ) { + $dt->set( year => $+{year} ); + } + } + else { + say "--date must be specified as DD.MM.[YYYY]"; + exit 1; + } + } + if ($time) { + if ( $time =~ m{ ^ (?<hour> \d{1,2} ) : (?<minute> \d{1,2} ) $ }x ) { + $dt->set( + hour => $+{hour}, + minute => $+{minute}, + second => 0, + ); + } + else { + say "--time must be specified as HH:MM"; + exit 1; + } + } + $opt{datetime} = $dt; +} + +my $status = Travel::Status::DE::HAFAS->new(%opt); + sub show_help { my ($code) = @_; @@ -82,27 +157,29 @@ sub show_version { exit 0; } -sub parse_mot_options { +sub spacer { + my ($len) = @_; + return ( $len % 2 ? q { } : q{} ) . ( q{ ·} x ( $len / 2 ) ); +} +sub parse_mot_options { my $default_yes = 1; - if ( $types and $hafas_url ) { - say STDERR 'The options -u and -m cannot be combined. Discarding -m'; - return; - } - for my $type ( split( qr{,}, $types ) ) { if ( $type eq 'help' or $type eq 'list' or $type eq q{?} ) { - if ( not $hafas_url ) { - $service //= 'DB'; - } + $service //= 'DB'; my $desc = Travel::Status::DE::HAFAS::get_service($service); if ($desc) { - my @mots = @{ $desc->{productbits} }; - @mots = grep { $_ ne 'x' } @mots; - @mots = uniq @mots; - @mots = sort @mots; - say join( "\n", @mots ); + for my $mot ( @{ $desc->{productbits} } ) { + if ( ref($mot) eq 'ARRAY' ) { + if ( $mot->[0] ne '_' ) { + printf( "%-10s %s\n", @{$mot} ); + } + } + elsif ( $mot ne '_' ) { + say $mot; + } + } exit 0; } else { @@ -127,6 +204,37 @@ sub show_similar_stops { for my $c (@candidates) { printf( "%s (%s)\n", $c->{name}, $c->{id} ); } + return; + } + my $hafas = Travel::Status::DE::HAFAS->new( + locationSearch => $opt{station}, + developer_mode => $developer_mode, + service => $service, + language => $language, + ); + if ( $hafas->results ) { + say 'You might want to try one of the following stops:'; + for my $r ( $hafas->results ) { + printf( "%s (%s)\n", $r->name, $r->eva ); + } + } + return; +} + +sub journey_has_via { + my ( $journey, $via ) = @_; + + if ( $via =~ m{ ^ [0-9,]+ $ }x ) { + for my $eva ( split( qr{,}, $via ) ) { + if ( my $stop = first { $_->loc->eva == $eva } $journey->route ) { + return $stop; + } + } + return; + } + + if ( my $stop = first { $_->loc->name =~ m{$via}io } $journey->route ) { + return $stop; } return; } @@ -146,11 +254,18 @@ sub display_result { for my $line (@lines) { - my $d = $line->[6]; - if ( $d->messages ) { - print "\n"; - for my $msg ( $d->messages ) { - printf( "# %s\n", $msg ); + my $d = $line->[6]; + my $first_message = 1; + for my $msg ( $d->messages ) { + if ( $msg->ref_count == 0 ) { + if ($first_message) { + print "\n"; + $first_message = 0; + } + if ( $msg->short ) { + printf( "# %s\n", $msg->short ); + } + printf( "# %s\n", $msg->text ); } } @@ -167,38 +282,388 @@ sub display_result { return; } +sub display_occupancy { + my ($occupancy) = @_; + + if ( not $occupancy ) { + return q{ }; + } + if ( $occupancy == 1 ) { + return q{.}; + } + if ( $occupancy == 2 ) { + return q{o}; + } + if ( $occupancy == 3 ) { + return q{*}; + } + if ( $occupancy == 4 ) { + return q{!}; + } + return q{?}; +} + +sub format_delay { + my ( $delay, $len ) = @_; + if ( $delay and $len ) { + return sprintf( "(%+${len}d)", $delay ); + } + return q{}; +} + if ( my $err = $status->errstr ) { say STDERR "Request error: ${err}"; - if ( $status->errcode and $status->errcode eq 'H730' ) { + if ( $status->errcode + and ( $status->errcode eq 'H730' or $status->errcode eq 'LOCATION' ) + and not $raw_json_output ) + { show_similar_stops(); } exit 2; } -for my $d ( $status->results() ) { - - if ( $ignore_late and $d->delay ) { - next; - } - - push( - @output, - [ - $d->time, - $d->is_cancelled - ? 'CANCELED' - : ( $d->delay ? sprintf( '%+d', $d->delay ) : q{} ), - $d->train, - $d->route_end, - ( $d->platform // q{} ) . ( $d->is_changed_platform ? ' !' : q{} ), - $d->info, - $d - ] - ); +if ($raw_json_output) { + say JSON->new->convert_blessed->encode( $status->{raw_json} ); + exit 0; +} + +if ($json_output) { + if ( $opt{journey} ) { + say JSON->new->convert_blessed->encode( $status->result ); + } + else { + say JSON->new->convert_blessed->encode( [ $status->results ] ); + } + exit 0; +} + +if ( $opt{journeyMatch} ) { + if ( scalar $status->results == 1 ) { + my ($journey) = $status->results; + $opt{journey} = { id => $journey->id }; + delete $opt{journeyMatch}; + $status = Travel::Status::DE::HAFAS->new(%opt); + if ( my $err = $status->errstr ) { + say STDERR "Request error: ${err}"; + if ( + $status->errcode + and + ( $status->errcode eq 'H730' or $status->errcode eq 'LOCATION' ) + and not $raw_json_output + ) + { + show_similar_stops(); + } + exit 2; + } + } + else { + for my $result ( $status->results ) { + my $start = ( $result->route )[0]; + my $end = ( $result->route )[-1]; + say $result->id; + print $result->name; + if ( $result->number ) { + printf( " | Nr %s", $result->number ); + } + if ( $result->line_no ) { + printf( " | Linie %s", $result->line_no ); + } + say q{}; + printf( "%s ab %s\n", + $start->dep->strftime('%H:%M'), + $start->loc->name ); + printf( "%s an %s\n\n", + $end->arr->strftime('%H:%M'), + $end->loc->name ); + } + exit 0; + } +} + +if ( $opt{geoSearch} ) { + for my $result ( $status->results ) { + printf( + "%5.1f km %8d %s\n", + $result->distance_m * 1e-3, + $result->eva, $result->name + ); + } + exit 0; +} +elsif ( $opt{locationSearch} ) { + for my $result ( $status->results ) { + printf( "%8d %s\n", $result->eva, $result->name ); + } + exit 0; +} +elsif ( $opt{journey} ) { + my $result = $status->result; + my @prods; + my @directions; + my $prev_prod = 0; + + printf( "%s → %s", $result->name, $result->route_end ); + if ( $result->number ) { + printf( " / Nr %s", $result->number ); + } + if ( $result->line_no ) { + printf( " / Linie %s", $result->line_no ); + } + printf( "\nFahrt %s am %s\n", + $result->id, ( $result->route )[0]->sched_dep->strftime('%d.%m.%Y') ); + + my $delay_len = 0; + my $delay_fmt = 0; + my $occupancy_len = 0; + my $stop_len = 0; + for my $stop ( $result->route ) { + if ( $stop->delay ) { + $delay_len = max( $delay_len, length( $stop->delay ) + 1 ); + } + if ( $stop->load and ( $stop->load->{FIRST} or $stop->load->{SECOND} ) ) + { + $occupancy_len = 2; + } + if ( length( $stop->loc->name ) > $stop_len ) { + $stop_len = length( $stop->loc->name ); + } + my $prod = $stop->prod_dep // $stop->prod_arr; + if ( $prod and $prod != $prev_prod ) { + push( @prods, $prod ); + $prev_prod = $prod; + } + if ( $stop->direction ) { + push( @directions, $stop->direction ); + } + } + if ($delay_len) { + $delay_fmt = $delay_len + 3; + } + + if ( $result->operators ) { + printf( "Betrieb: %s\n", join( q{, }, $result->operators ) ); + } + + $prev_prod = 0; + + my $desc = Travel::Status::DE::HAFAS::get_service($service) // {}; + my $now + = DateTime->now( time_zone => $desc->{time_zone} // 'Europe/Berlin' ); + my $mark_stop = 0; + for my $i ( reverse 1 .. scalar $result->route ) { + my $stop = ( $result->route )[ $i - 1 ]; + if ( not $stop->dep_cancelled and $stop->dep and $now <= $stop->dep ) { + $mark_stop = $stop; + } + elsif ( not $stop->arr_cancelled and $stop->arr and $now <= $stop->arr ) + { + $mark_stop = $stop; + } + } + + my $message_id = 1; + + print "\n"; + for my $stop ( $result->route ) { + my $msg_line = q{}; + for my $message ( $stop->messages ) { + if ( $message->ref_count > 0 + and $message->code ne + 'text.journeystop.product.or.direction.changes.stop.message' + and $message->text ne 'Halt entfällt' ) + { + if ( not $message->{id} ) { + $message->{id} = $message_id++; + } + $msg_line .= sprintf( ' (%d)', $message->{id} ); + } + } + + my $prod_line = q{}; + if ( @prods > 1 ) { + my $prod = $stop->prod_dep // $stop->prod_arr; + if ( $prod and $prod != $prev_prod ) { + $prod_line + = sprintf( " %s (%s)", $prod->name, $prod->operator ); + $prev_prod = $prod; + } + } + + my $dir_line = q{}; + if ( @directions > 1 and $stop->direction ) { + $dir_line = ' → ' . $stop->direction; + } + + my $tz_line = q{}; + if ( $stop->tz_offset and ( $stop->arr or $stop->dep ) ) { + $tz_line = ( $prod_line or $dir_line ) ? q{ · } : q{ }; + $tz_line .= 'local '; + if ( $stop->arr ) { + $tz_line + .= $stop->arr->clone->add( minutes => $stop->tz_offset ) + ->strftime('%H:%M'); + } + if ( $stop->arr and $stop->dep ) { + $tz_line .= ' → '; + } + if ( $stop->dep ) { + $tz_line + .= $stop->dep->clone->add( minutes => $stop->tz_offset ) + ->strftime('%H:%M'); + } + $tz_line .= q{ }; + } + + printf( +"%s%5s %s %5s %-${delay_fmt}s%${occupancy_len}s%-${occupancy_len}s %s%s%s%s%s%s%s\n", + $stop == $mark_stop ? $output_bold : q{}, + $stop->arr_cancelled ? '--:--' + : ( $stop->arr ? $stop->arr->strftime('%H:%M') : q{} ), + ( $stop->arr and $stop->dep ) ? '→' : q{ }, + $stop->dep_cancelled ? '--:--' + : ( $stop->dep ? $stop->dep->strftime('%H:%M') : q{} ), + format_delay( $stop->delay, $delay_len ), + $stop->load->{FIRST} ? display_occupancy( $stop->load->{FIRST} ) + : q{}, + $stop->load->{SECOND} ? display_occupancy( $stop->load->{SECOND} ) + : q{}, + $stop->loc->name, + $stop == $mark_stop ? $output_reset : q{}, + ( $tz_line or $prod_line or $dir_line or $msg_line ) + ? spacer( $stop_len + 1 - length( $stop->loc->name ) ) + : q{}, + $prod_line, + $dir_line, + $tz_line, + $msg_line, + ); + } + + for my $msg ( $result->messages ) { + if ( $msg->code eq + 'text.journeystop.product.or.direction.changes.journey.message' ) + { + next; + } + say ''; + if ( $msg->short ) { + printf( "%s\n", $msg->short ); + } + printf( "%s\n", $msg->text ); + } + + for my $msg ( $status->messages ) { + if ( $msg->{id} ) { + say ''; + if ( $msg->short ) { + printf( "(%d) %s\n", $msg->{id}, $msg->short ); + } + printf( "(%d) %s\n", $msg->{id}, $msg->text ); + } + } + exit 0; +} + +my @results = map { $_->[1] } + sort { $a->[0] <=> $b->[0] } + map { [ $_->datetime->epoch, $_ ] } $status->results; + +if ($via) { + @results = grep { journey_has_via( $_, $via ) } @results; +} + +my $delay_len = 0; +my $occupancy_len = 0; +my $offset_len = 0; +for my $d (@results) { + if ( $d->delay ) { + $delay_len = max( $delay_len, length( $d->delay ) + 1 ); + } + if ( $d->load and ( $d->load->{FIRST} or $d->load->{SECOND} ) ) { + $occupancy_len = 2; + } + if ( $d->tz_offset ) { + $offset_len = 1; + } +} + +my $message_id = 1; +for my $m ( $status->messages ) { + if ( $m->ref_count > 0 ) { + $m->{id} = $message_id++; + } +} + +for my $d (@results) { + + my $info_line = q{}; + + for my $message ( $d->messages ) { + if ( $message->ref_count > 0 ) { + $message->{show} = 1; + $info_line = sprintf( '(%d) %s', $message->{id}, $info_line ); + } + } + + if ( $d->load ) { + $info_line + = display_occupancy( $d->load->{FIRST} ) + . display_occupancy( $d->load->{SECOND} ) . ' ' + . $info_line; + } + + if ($show_jid) { + $info_line = $d->id . ' ' . $info_line; + } + + my $entry = [ + ( $d->is_cancelled ? '--:--' : $d->datetime->strftime('%H:%M') ) + . ( $d->tz_offset ? q{*} : ( q{ } x $offset_len ) ), + $d->is_cancelled + ? q{} + : format_delay( $d->delay, $delay_len ), + $d->name, + $d->route_end, + ( $d->platform // q{} ) . ( $d->is_changed_platform ? ' !' : q{} ), + $info_line, + $d + ]; + + if ($via) { + my $stop = journey_has_via( $d, $via ); + + # HAFAS does not provide real-time data for route entries, so we have to guesstimate the arrival time + $entry->[0] .= ' → ' + . ( + $stop->arr_cancelled + ? '--:--' + : $stop->arr->clone->add( minutes => $d->delay // 0 ) + ->strftime('%H:%M') + ); + } + + push( @output, $entry, ); } display_result(@output); +if ($offset_len) { + printf( "\n* reported for %s; local time differs\n", + $status->get_active_service->{time_zone} // 'Europe/Berlin' ); +} + +for my $m ( $status->messages ) { + if ( $m->ref_count > 0 and $m->{show} ) { + if ( $m->short ) { + printf( "\n# (%d) %s\n# %s\n", $m->{id}, $m->short, $m->text ); + } + else { + printf( "\n# (%d) %s\n", $m->{id}, $m->text ); + } + } +} + __END__ =head1 NAME @@ -207,21 +672,70 @@ hafas-m - Interface to the DeutscheBahn/HAFAS online departure monitor =head1 SYNOPSIS -B<hafas-m> [B<-d> I<date>] [B<-t> I<time>] [B<-m> I<motlist>] -[B<-s> I<service> | B<-u> I<url>] I<station> +B<hafas-m> [B<-a>] [B<-d> I<date>] [B<-t> I<time>] [B<-m> I<motlist>] +[B<-v> I<via>] [B<-s> I<service>] [B<-l> I<language>] I<station> + +B<hafas-m> [B<-s> I<service>] B<?>I<query>|I<lat>B<:>I<lon> + +B<hafas-m> [B<-s> I<service>] [B<-l> I<language>] B<!>I<query>|I<journeyID> =head1 VERSION -version 2.03 +version 6.03 =head1 DESCRIPTION -hafas-m is an interface to HAFAS-based departure monitors, for instance the -one available at L<http://reiseauskunft.bahn.de/bin/bhftafel.exe/dn>. +hafas-m is an interface to HAFAS public transport services such as the one +operated by Deutsche Bahn. + +It has four operating modes that depend on the contents of its argument. + +=head2 Arrival/Departure Monitor (I<station>) + +Show departures (or arrivals) at I<station>, optionally filtered by mode of +transport. I<station> may be given as a station name or HAFAS station ID. +For each train, B<hafas-m> shows + +=over + +=item * estimated departure (or arrival) time, + +=item * delay, if known, + +=item * trip name, number, or line, -It requests all departures at I<station> (optionally filtered by date, time, -route and means of transport) and lists them on stdout, similar to the big -departure screens installed at most main stations. +=item * direction / destination, + +=item * platform, if known (B<!> indicates a platform change), and + +=item * expected occupancy of first and second class, if known. + +=back + +Times are always given in the selected service's default time zone (see +B<--list>). Times that differ from the queried stop's local time are marked +with C<< * >>. Occupancy indicators are, from least occupied to fully booked: +B<.> B<o> B<*> B<!>. + +=head2 Location Search (B<?>I<query>|I<lat>B<:>I<lon>) + +List stations that match I<query> or that are located in the vicinity of +I<lat>B<:>I<lon> geocoordinates with EVA ID and name. + +=head2 Trip Search (B<!>I<query>) + +Show trip details (see below) for the train number provided in I<query> +(e.g. "ICE 205" or "S 31111") if it resolves into a single journey ID. +Otherwise, list all journey IDs that match I<query>. + +=head2 Trip Details (I<journeyID>) + +List intermediate stops of I<journeyID> with arrival/departure time, delay (if +available), occupancy (if available), and stop name. Also includes +line/journey, operator, and heading information. + +Times are reported in the selected HAFAS service's default time zone (typically +Europe/Berlin). If a stop's local time differs, it is also provided. =head1 OPTIONS @@ -233,22 +747,31 @@ Show arrivals instead of departures, including trains ending at the specified station. Note that this causes the output to display the start instead of the end station. -=item B<-d>, B<--date> I<dd>.I<mm>.I<yyyy> +=item B<-d>, B<--date> I<dd>.I<mm>.[I<yyyy>] Date to list departures for. Default: today. -=item B<-l>, B<--lang> B<d>|B<e>|B<i>|B<n> +=item B<-j>, B<--with-jid> -Set language used for additional information. Supports B<d>eutsch (default), -B<e>nglish, B<i>talian and dutch (B<n>), depending on the used service. +Show journey IDs in arrival/departure board. These can be used to obtain +details on individual journeys with subsequent B<hafas-m> invocations. -=item B<-L>, B<--ignore-late> +=item B<--json> -Do not display delayed trains. +Print result(s) as JSON and exit. This is a dump of internal data structures +and not guaranteed to remain stable between minor versions. Please use the +Travel::Status::DE::HAFAS(3pm) module if you need a proper API. + +=item B<-l>, B<--language> I<language> + +Request free-text messages to be provided in I<language>. +See B<--list> for a list of languages supported by individual HAFAS instances. +Note that requesting an invalid/unsupported language may lead to garbage output. =item B<--list> -List known HAFAS installations. See also B<--service> and B<--url>. +List known HAFAS instances and exit. Use B<-s>|B<--service> to select a +service from this list for a HAFAS request. =item B<-m>, B<--mot> I<motlist> @@ -262,6 +785,11 @@ To show them exclusively, set I<motlist> to I<mot1>,I<mot2>,... The I<mot> types depend on the used service. Use C<< -m help >> to list them. +=item B<--raw-json> + +Print unprocessed HAFAS response as JSON and exit. +Useful for debugging and development purposes. + =item B<-s>, B<--service> I<service> Request arrivals/departures using the API provided by I<service>, defaults @@ -269,26 +797,25 @@ to DB (Deutsche Bahn). See B<--list> for a list of known services. =item B<-t>, B<--time> I<hh>:I<mm> -Time to list departures for. Default: now. - -=item B<-u>, B<--url> I<url> +Time to list departures for. Must be specified in the selected HAFAS +service's default time zone, see B<--list>. Default: now. -Request arrivals/departures using the API entry point at I<url>, defaults to -C<< http://reiseauskunft.bahn.de/bin/bhftafel.exe >>. Note that the language -and output selection suffix (e.g. "/dn") must not be included here. +=item B<-v>, B<--via> I<stopname>|I<eva1>,I<eva2>,... -Again, see B<--list> for a list of known URLs. Unknown URLs are also -supported, though note that B<--mot> will not work when using this opton. +Only show departures that pass by I<stopname> (or arivals that have passed by +I<stopname>). If I<stopname> is given as a list of numeric EVA IDs, only +arrivals/departures with an exact EVA ID match are shown. Otherwise I<stopname> +is treated as a regular expression and matched against stop names. =item B<-V>, B<--version> -Show version information. +Show version information and exit. =back =head1 EXIT STATUS -Zero unless things went wrong. +0 upon success, 1 upon internal error, 2 upon backend error. =head1 CONFIGURATION @@ -302,17 +829,23 @@ None. =item * LWP::UserAgent(3pm) -=item * XML::LibXML(3pm) - =back =head1 BUGS AND LIMITATIONS -The non-default services (anything other than DB) are not well tested. +=over + +=item * The non-default services (anything other than DB) are not well-tested. + +=item * HAFAS does not provide real-time data for routes of stationboard +entries. Hence, B<--via> estimates the arrival time from scheduled +departure and departure delay + +=back =head1 AUTHOR -Copyright (C) 2015-2017 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt> +Copyright (C) 2015-2023 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt> =head1 LICENSE diff --git a/cpanfile b/cpanfile new file mode 100644 index 0000000..3473989 --- /dev/null +++ b/cpanfile @@ -0,0 +1,17 @@ +requires 'Class::Accessor'; +requires 'DateTime'; +requires 'DateTime::Format::Strptime'; +requires 'Digest::MD5'; +requires 'Getopt::Long'; +requires 'JSON'; +requires 'List::MoreUtils'; +requires 'List::Util'; +requires 'LWP::UserAgent'; +requires 'LWP::Protocol::https'; + +on test => sub { + requires 'File::Slurp'; + requires 'Test::Compile'; + requires 'Test::More'; + requires 'Test::Pod'; +}; diff --git a/lib/Travel/Status/DE/DeutscheBahn.pm b/lib/Travel/Status/DE/DeutscheBahn.pm index 4809f55..bdf961d 100644 --- a/lib/Travel/Status/DE/DeutscheBahn.pm +++ b/lib/Travel/Status/DE/DeutscheBahn.pm @@ -6,7 +6,7 @@ use 5.014; use parent 'Travel::Status::DE::HAFAS'; -our $VERSION = '2.03'; +our $VERSION = '6.03'; sub new { my ( $class, %opt ) = @_; @@ -40,7 +40,7 @@ monitor operated by Deutsche Bahn for my $departure ($status->results) { printf( "At %s: %s to %s from platform %s\n", - $departure->time, + $departure->datetime->strftime('%H:%M'), $departure->line, $departure->destination, $departure->platform, @@ -49,13 +49,13 @@ monitor operated by Deutsche Bahn =head1 VERSION -version 2.03 +version 6.03 =head1 DESCRIPTION Travel::Status::DE::DeutscheBahn is an interface to the Deutsche Bahn departure monitor available at -L<http://reiseauskunft.bahn.de/bin/bhftafel.exe/dn>. +L<https://reiseauskunft.bahn.de/bin/mgate.exe>. 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 @@ -79,25 +79,19 @@ and other methdos. =head1 DIAGNOSTICS -None. +See Travel::Status::DE::HAFAS(3pm). =head1 DEPENDENCIES =over -=item * Class::Accessor(3pm) - -=item * LWP::UserAgent(3pm) - =item * Travel::Status::DE::HAFAS(3pm) -=item * XML::LibXML(3pm) - =back =head1 BUGS AND LIMITATIONS -Unknown. +See Travel::Status::DE::HAFAS(3pm). =head1 SEE ALSO @@ -105,7 +99,7 @@ Travel::Status::DE::HAFAS(3pm). =head1 AUTHOR -Copyright (C) 2015-2017 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt> +Copyright (C) 2015-2022 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt> =head1 LICENSE 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 diff --git a/lib/Travel/Status/DE/HAFAS/Journey.pm b/lib/Travel/Status/DE/HAFAS/Journey.pm new file mode 100644 index 0000000..2a5d4c0 --- /dev/null +++ b/lib/Travel/Status/DE/HAFAS/Journey.pm @@ -0,0 +1,682 @@ +package Travel::Status::DE::HAFAS::Journey; + +# vim:foldmethod=marker + +use strict; +use warnings; +use 5.014; + +use parent 'Class::Accessor'; +use DateTime::Format::Strptime; +use List::Util qw(any uniq); +use Travel::Status::DE::HAFAS::Stop; + +our $VERSION = '6.03'; + +Travel::Status::DE::HAFAS::Journey->mk_ro_accessors( + qw(datetime sched_datetime rt_datetime tz_offset + is_additional is_cancelled is_partially_cancelled + station station_eva platform sched_platform rt_platform operator + product product_at + id name type type_long class number line line_no load delay + route_end route_start origin destination direction) +); + +# {{{ Constructor + +sub new { + my ( $obj, %opt ) = @_; + + my @icoL = @{ $opt{common}{icoL} // [] }; + my @tcocL = @{ $opt{common}{tcocL} // [] }; + my @remL = @{ $opt{common}{remL} // [] }; + my @himL = @{ $opt{common}{himL} // [] }; + + my $prodL = $opt{prodL}; + my $locL = $opt{locL}; + my $hafas = $opt{hafas}; + my $journey = $opt{journey}; + + my $date = $opt{date} // $journey->{date}; + + my $direction = $journey->{dirTxt}; + my $jid = $journey->{jid}; + + my $is_cancelled = $journey->{isCncl}; + my $partially_cancelled = $journey->{isPartCncl}; + + my $product = $prodL->[ $journey->{prodX} ]; + + my @messages; + for my $msg ( @{ $journey->{msgL} // [] } ) { + if ( $msg->{type} eq 'REM' and defined $msg->{remX} ) { + push( @messages, $hafas->add_message( $remL[ $msg->{remX} ] ) ); + } + elsif ( $msg->{type} eq 'HIM' and defined $msg->{himX} ) { + push( @messages, $hafas->add_message( $himL[ $msg->{himX} ], 1 ) ); + } + else { + say "Unknown message type $msg->{type}"; + } + } + + my $datetime_ref; + + if ( @{ $journey->{stopL} // [] } or $journey->{stbStop} ) { + my ( $date_ref, $parse_fmt ); + if ( $jid =~ /#/ ) { + + # ÖBB Journey ID - technically we ought to use Europe/Vienna tz + # but let's not get into that... + $date_ref = ( split( /#/, $jid ) )[12]; + $parse_fmt = '%d%m%y'; + if ( length($date_ref) < 5 ) { + warn( +"HAFAS, not even once -- midnight crossing may be bogus -- date_ref $date_ref" + ); + } + elsif ( length($date_ref) == 5 ) { + $date_ref = "0${date_ref}"; + } + } + else { + # DB Journey ID + $date_ref = ( split( qr{[|]}, $jid ) )[4]; + $parse_fmt = '%d%m%Y'; + if ( length($date_ref) < 7 ) { + warn( +"HAFAS, not even once -- midnight crossing may be bogus -- date_ref $date_ref" + ); + } + elsif ( length($date_ref) == 7 ) { + $date_ref = "0${date_ref}"; + } + } + $datetime_ref = DateTime::Format::Strptime->new( + pattern => $parse_fmt, + time_zone => $hafas->get_active_service->{time_zone} + // 'Europe/Berlin' + )->parse_datetime($date_ref); + } + + my @stops; + my $route_end; + for my $stop ( @{ $journey->{stopL} // [] } ) { + my $loc = $locL->[ $stop->{locX} ]; + + my $stopref = { + loc => $loc, + stop => $stop, + common => $opt{common}, + prodL => $prodL, + hafas => $hafas, + date => $date, + datetime_ref => $datetime_ref, + }; + + push( @stops, $stopref ); + + $route_end = $loc->name; + } + + if ( $journey->{stbStop} ) { + if ( $hafas->{arrivals} ) { + $route_end = $stops[0]->{name}; + pop(@stops); + } + else { + shift(@stops); + } + } + + my $ref = { + id => $jid, + product => $product, + name => $product->name, + number => $product->number, + line => $product->name, + line_no => $product->line_no, + type => $product->type, + type_long => $product->type_long, + class => $product->class, + operator => $product->operator, + direction => $direction, + is_cancelled => $is_cancelled, + is_partially_cancelled => $partially_cancelled, + route_end => $route_end // $direction, + messages => \@messages, + route => \@stops, + }; + + if ( $journey->{stbStop} ) { + if ( $hafas->{arrivals} ) { + $ref->{origin} = $ref->{route_end}; + $ref->{is_cancelled} ||= $journey->{stbStop}{aCncl}; + } + else { + $ref->{destination} = $ref->{route_end}; + $ref->{is_cancelled} ||= $journey->{stbStop}{dCncl}; + } + $ref->{is_additional} = $journey->{stbStop}{isAdd}; + } + else { + $ref->{route_start} = $stops[0]{loc}->name; + } + + bless( $ref, $obj ); + + if ( $journey->{stbStop} ) { + $ref->{station} = $locL->[ $journey->{stbStop}{locX} ]->name; + $ref->{station_eva} = 0 + $locL->[ $journey->{stbStop}{locX} ]->eva; + $ref->{sched_platform} = $journey->{stbStop}{dPlatfS} + // $journey->{stbStop}{dPltfS}{txt}; + $ref->{rt_platform} = $journey->{stbStop}{dPlatfR} + // $journey->{stbStop}{dPltfR}{txt}; + $ref->{platform} = $ref->{rt_platform} // $ref->{sched_platform}; + + my $datetime_s = Travel::Status::DE::HAFAS::Stop::handle_day_change( + $ref, + input => + $journey->{stbStop}{ $hafas->{arrivals} ? 'aTimeS' : 'dTimeS' }, + offset => $journey->{stbStop}{ + $hafas->{arrivals} + ? 'aTZOffset' + : 'dTZOffset' + }, + date => $date, + strp_obj => $hafas->{strptime_obj}, + ref => $datetime_ref, + ); + + my $datetime_r = Travel::Status::DE::HAFAS::Stop::handle_day_change( + $ref, + input => + $journey->{stbStop}{ $hafas->{arrivals} ? 'aTimeR' : 'dTimeR' }, + offset => $journey->{stbStop}{ + $hafas->{arrivals} + ? 'aTZOffset' + : 'dTZOffset' + }, + date => $date, + strp_obj => $hafas->{strptime_obj}, + ref => $datetime_ref, + ); + + my $delay + = $datetime_r + ? ( $datetime_r->epoch - $datetime_s->epoch ) / 60 + : undef; + + $ref->{sched_datetime} = $datetime_s; + $ref->{rt_datetime} = $datetime_r; + $ref->{datetime} = $datetime_r // $datetime_s; + $ref->{delay} = $delay; + + if ( $ref->{delay} ) { + $ref->{datetime} = $ref->{rt_datetime}; + } + else { + $ref->{datetime} = $ref->{sched_datetime}; + } + + my %tco; + for my $tco_id ( @{ $journey->{stbStop}{dTrnCmpSX}{tcocX} // [] } ) { + my $tco_kv = $tcocL[$tco_id]; + $tco{ $tco_kv->{c} } = $tco_kv->{r}; + } + if (%tco) { + $ref->{load} = \%tco; + } + } + if ( $opt{polyline} ) { + $ref->{polyline} = $opt{polyline}; + } + + return $ref; +} + +# }}} + +# {{{ Accessors + +# Legacy +sub station_uic { + my ($self) = @_; + return $self->{station_eva}; +} + +sub is_changed_platform { + my ($self) = @_; + + if ( defined $self->{rt_platform} and defined $self->{sched_platform} ) { + if ( $self->{rt_platform} ne $self->{sched_platform} ) { + return 1; + } + return 0; + } + if ( defined $self->{rt_platform} ) { + return 1; + } + + return 0; +} + +sub messages { + my ($self) = @_; + + if ( $self->{messages} ) { + return @{ $self->{messages} }; + } + return; +} + +sub operators { + my ($self) = @_; + + if ( $self->{operators} ) { + return @{ $self->{operators} }; + } + + $self->{operators} = [ + uniq map { ( $_->prod_arr // $_->prod_dep )->operator } grep { + ( $_->prod_arr or $_->prod_dep ) + and ( $_->prod_arr // $_->prod_dep )->operator + } $self->route + ]; + + return @{ $self->{operators} }; +} + +sub polyline { + my ($self) = @_; + + if ( $self->{polyline} ) { + return @{ $self->{polyline} }; + } + return; +} + +sub route { + my ($self) = @_; + + if ( $self->{route} ) { + if ( $self->{route}[0] and $self->{route}[0]{stop} ) { + $self->{route} + = [ map { Travel::Status::DE::HAFAS::Stop->new( %{$_} ) } + @{ $self->{route} } ]; + } + return @{ $self->{route} }; + } + return; +} + +sub route_interesting { + my ( $self, $max_parts ) = @_; + + my @via = $self->route; + my ( @via_main, @via_show, $last_stop ); + $max_parts //= 3; + + # Centraal: dutch main station (Hbf in .nl) + # HB: swiss main station (Hbf in .ch) + # hl.n.: czech main station (Hbf in .cz) + for my $stop (@via) { + if ( $stop->loc->name + =~ m{ HB $ | hl\.n\. $ | Hbf | Hauptbahnhof | Bf | Bahnhof | Centraal | Flughafen }x + ) + { + push( @via_main, $stop ); + } + } + $last_stop = pop(@via); + + if ( @via_main and $via_main[-1]->loc->name eq $last_stop->loc->name ) { + pop(@via_main); + } + if ( @via and $via[-1]->loc->name eq $last_stop->loc->name ) { + pop(@via); + } + + if ( @via_main and @via and $via[0]->loc->name eq $via_main[0]->loc->name ) + { + shift(@via_main); + } + + if ( @via < $max_parts ) { + @via_show = @via; + } + else { + if ( @via_main >= $max_parts ) { + @via_show = ( $via[0] ); + } + else { + @via_show = splice( @via, 0, $max_parts - @via_main ); + } + + while ( @via_show < $max_parts and @via_main ) { + my $stop = shift(@via_main); + if ( any { $_->loc->name eq $stop->loc->name } @via_show + or $stop->loc->name eq $last_stop->loc->name ) + { + next; + } + push( @via_show, $stop ); + } + } + + return @via_show; + +} + +sub product_at { + my ( $self, $req_stop ) = @_; + for my $stop ( $self->route ) { + if ( $stop->loc->name eq $req_stop or $stop->loc->eva eq $req_stop ) { + return $stop->prod_dep // $stop->prod_arr; + } + } + return; +} + +sub TO_JSON { + my ($self) = @_; + + my $ret = { %{$self} }; + + for my $k ( keys %{$ret} ) { + if ( ref( $ret->{$k} ) eq 'DateTime' ) { + $ret->{$k} = $ret->{$k}->epoch; + } + } + $ret->{route} = [ map { $_->TO_JSON } $self->route ]; + + return $ret; +} + +# }}} + +1; + +__END__ + +=head1 NAME + +Travel::Status::DE::HAFAS::Journey - Information about a single +journey received by Travel::Status::DE::HAFAS + +=head1 SYNOPSIS + + for my $departure ($status->results) { + printf( + "At %s: %s to %s from platform %s\n", + $departure->datetime->strftime('%H:%M'), + $departure->line, + $departure->destination, + $departure->platform, + ); + } + + # or (depending on module setup) + for my $arrival ($status->results) { + printf( + "At %s: %s from %s on platform %s\n", + $arrival->datetime->strftime('%H:%M'), + $arrival->line, + $arrival->origin, + $arrival->platform, + ); + } + +=head1 VERSION + +version 6.03 + +=head1 DESCRIPTION + +Travel::Status::DE::HAFAS::Journey describes a single journey. It is either +a station-specific arrival/departure obtained by a stationboard query, or a +train journey that does not belong to a specific station. + +stationboard-specific accessors are annotated with "(station only)" and return +undef for non-station journeys. All date and time entries refer to the +backend time zone (Europe/Berlin in most cases) and do not take local time +into account; see B<tz_offset> for the latter. + +=head1 METHODS + +=head2 ACCESSORS + +=over + +=item $journey->name + +Journey or line name, either in a format like "Bus SB16" (Bus line +SB16) or "RE 10111" (RegionalExpress train 10111, no line information). May +contain extraneous whitespace characters. + +=item $journey->type + +Type of this journey, e.g. "S" for S-Bahn, "RE" for Regional Express +or "STR" for tram / StraE<szlig>enbahn. + +=item $journey->type_long + +Long type of this journey, e.g. "S-Bahn" or "Regional-Express". + +=item $journey->class + +An integer identifying the the mode of transport class. +Semantics depend on backend, e.g. "1" and "2" for long-distance trains and +"4" and "8" for regional trains. + +=item $journey->line + +Journey or line name, either in a format like "Bus SB16" (Bus line +SB16), "RE 42" (RegionalExpress train 42) or "IC 2901" (InterCity train 2901, +no line information). May contain extraneous whitespace characters. Note that +this accessor does not return line information for IC/ICE/EC services, even if +it is available. Use B<line_no> for those. + +=item $journey->line_no + +Line identifier, or undef if it is unknown. +The line identifier may be a single number such as "11" (underground train +line U 11), a single word (e.g. "AIR") or a combination (e.g. "SB16"). +May also provide line numbers of IC/ICE services. + +=item $journey->number + +Journey number (e.g. train number), or undef if it is unknown. + +=item $journey->id + +HAFAS-internal journey ID. + +=item $journey->rt_datetime (station only) + +DateTime object indicating the actual arrival/departure date and time. +undef if no real-time data is available. + +=item $journey->sched_datetime (station only) + +DateTime object indicating the scheduled arrival/departure date and time. +undef if no schedule data is available. + +=item $journey->datetime (station only) + +DateTime object indicating the arrival/departure date and time. +Real-time data if available, schedule data otherwise. +undef if neither is available. + +=item $journey->tz_offset + +Offset between backend time zone (default: Europe/Berlin) and this journey's +time zone in minutes, if any. For instance, if the backend uses UTC+2 (CEST) +and the journey uses UTC+1 (IST), tz_offset is -60. Returns undef if both use +the same time zone (or rather, the same UTC offset). + +=item $journey->delay (station only) + +Delay in minutes, or undef if it is unknown. +Also returns undef if the arrival/departure has been cancelled. + +=item $journey->is_additional (station only) + +True if the journey's stop at the requested station is an unscheduled addition +to its route. + +=item $journey->is_cancelled + +True if the journey was cancelled, false otherwise. + +=item $journey->is_partially_cancelled + +True if part of the journey was cancelled, false otherwise. + +=item $journey->product + +Travel::Status::DE::HAFAS::Product(3pm) instance describing the product (mode +of transport, line number / ID, operator, ...) associated with this journey. +Note that journeys may be associated with multiple products -- see also +C<< $journey->route >> and C<< $stop->product >>. + +=item $journey->product_at(I<stop>) + +Travel::Status::DE::HAFAS::Product(3pm) instance describing the product +associated with I<stop> (name or EVA ID). Returns undef if product or I<stop> +are unknown. + +=item $journey->rt_platform (station only) + +Actual arrival/departure platform. +undef if no real-time data is available. + +=item $journey->sched_platform (station only) + +Scheduled arrival/departure platform. +undef if no scheduled platform is available. + +=item $journey->platform (station only) + +Arrival/Departure platform. Real-time data if available, schedule data +otherwise. May be undef. + +=item $journey->is_changed_platform (station only) + +True if the real-time platform is known and it is not the scheduled one. + +=item $journey->load (station only) + +Expected passenger load (i.e., how full the vehicle is) at the requested stop. +If known, returns a hashref that maps classes (typically FIRST/SECOND) to +load estimation numbers. The DB backend uses 1 (low to medium), 2 (high), +3 (very high), and 4 (exceptionally high, train is booked out). +Undef if unknown. + +=item $journey->messages + +List of Travel::Status::DE::HAFAS::Message(3pm) instances related to this +journey. Messages usually are service notices (e.g. "missing carriage") or +detailed delay reasons (e.g. "switch damage between X and Y, expect delays"). + +=item $journey->operator + +The operator responsible for this journey. Returns undef +if the backend does not provide an operator. Note that the operator may +change along the journey -- in this case, the returned operator depends on +the backend and appears to be the first one in most cases. + +=item $journey->operators + +List of all operators observed along the journey. + +=item $journey->station (station only) + +Name of the station at which this journey was requested. + +=item $journey->station_eva (station only) + +UIC/EVA ID of the station at which this journey was requested. + +=item $journey->route + +List of Travel::Status::DE::HAFAS::Stop(3pm) objects that describe individual +stops along the journey. In stationboard mode, the list only contains arrivals +prior to the requested station or departures after the requested station. In +journey mode, it contains the entire route. + +=item $journey->route_interesting([I<count>]) + +Up to I<count> (default: B<3>) parts of C<< $journey->route >> that may +be particularly helpful, e.g. main stations or airports. + +=item $journey->route_end + +Name of the last route station. In arrival mode, this is where the train +started; in all other cases, it is the terminus. + +=item $journey->destination + +Alias for route_end; only set when requesting departures in station mode. + +=item $journey->origin + +Alias for route_end; only set when requesting arrivals in station mode. + +=item $journey->direction + +Train direction; this is typically the text printed on the train itself. +May be different from destination / route_end and may change along the route, +see above. + +=item $journey->polyline (journey only) + +List of geocoordinates that describe the train's route. Only available if the +HAFAS object constructor was passed a true B<with_polyline> value. Each list +entry is a hash with the following keys. + +=over + +=item * lon (longitude) + +=item * lat (latitude) + +=item * name (name of stop at this location, if any. undef otherwise) + +=item * eva (EVA ID of stop at this location, if any. undef otherwise) + +=back + +Note that stop locations in B<polyline> may differ from the coordinates +returned in B<route>. This is a backend issue; Travel::Status::DE::HAFAS +simply passes the returned coordinates on. + +=back + +=head1 DIAGNOSTICS + +None. + +=head1 DEPENDENCIES + +=over + +=item Class::Accessor(3pm) + +=back + +=head1 BUGS AND LIMITATIONS + +None known. + +=head1 SEE ALSO + +Travel::Status::DE::HAFAS(3pm). + +=head1 AUTHOR + +Copyright (C) 2015-2023 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt> + +=head1 LICENSE + +This module is licensed under the same terms as Perl itself. diff --git a/lib/Travel/Status/DE/HAFAS/Location.pm b/lib/Travel/Status/DE/HAFAS/Location.pm new file mode 100644 index 0000000..fd25634 --- /dev/null +++ b/lib/Travel/Status/DE/HAFAS/Location.pm @@ -0,0 +1,127 @@ +package Travel::Status::DE::HAFAS::Location; + +use strict; +use warnings; +use 5.014; + +use parent 'Class::Accessor'; + +our $VERSION = '6.03'; + +Travel::Status::DE::HAFAS::Location->mk_ro_accessors( + qw(lid type name eva state lat lon distance_m weight)); + +sub new { + my ( $obj, %opt ) = @_; + + my $loc = $opt{loc}; + + my $ref = { + lid => $loc->{lid}, + type => $loc->{type}, + name => $loc->{name}, + eva => 0 + $loc->{extId}, + state => $loc->{state}, + lat => $loc->{crd}{y} * 1e-6, + lon => $loc->{crd}{x} * 1e-6, + + # only for geosearch requests + weight => $loc->{wt}, + distance_m => $loc->{dist}, + }; + + bless( $ref, $obj ); + + return $ref; +} + +sub TO_JSON { + my ($self) = @_; + + my $ret = { %{$self} }; + + return $ret; +} + +1; + +__END__ + +=head1 NAME + +Travel::Status::DE::HAFAS::Location - A single public transit location + +=head1 SYNOPSIS + + printf("Destination: %s (%8d)\n", $location->name, $location->eva); + +=head1 VERSION + +version 6.03 + +=head1 DESCRIPTION + +Travel::Status::DE::HAFAS::Location describes a HAFAS location that belongs to +a stop (e.g. on a journey's route) or has been returned as part of a +locationSearch or geoSearch request. + +=head1 METHODS + +=head2 ACCESSORS + +=over + +=item $location->name + +Location name, e.g. "Essen Hbf" or "Unter den Linden/B75, Tostedt". + +=item $location->eva + +EVA ID, e.g. 8000080. + +=item $location->lat + +Location latitude (WGS-84) + +=item $location->lon + +Location longitude (WGS-84) + +=item $location->distance_m (geoSearch) + +Distance in meters between the requested coordinates and this location. + +=item $location->weight (geoSearch, locationSearch) + +Weight / Relevance / Importance of this location using an unknown metric. +Higher values indicate more relevant locations. + +=back + +=head1 DIAGNOSTICS + +None. + +=head1 DEPENDENCIES + +=over + +=item Class::Accessor(3pm) + +=back + +=head1 BUGS AND LIMITATIONS + +None known. + +=head1 SEE ALSO + +Travel::Routing::DE::HAFAS(3pm). + +=head1 AUTHOR + +Copyright (C) 2023 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt> + +=head1 LICENSE + +This module is licensed under the same terms as Perl itself. diff --git a/lib/Travel/Status/DE/HAFAS/Message.pm b/lib/Travel/Status/DE/HAFAS/Message.pm new file mode 100644 index 0000000..ae2fa71 --- /dev/null +++ b/lib/Travel/Status/DE/HAFAS/Message.pm @@ -0,0 +1,185 @@ +package Travel::Status::DE::HAFAS::Message; + +use strict; +use warnings; +use 5.014; + +use parent 'Class::Accessor'; + +our $VERSION = '6.03'; + +Travel::Status::DE::HAFAS::Message->mk_ro_accessors( + qw(short type text code prio is_him ref_count)); + +sub new { + my ( $obj, %conf ) = @_; + + my $json = $conf{json}; + my $is_him = $conf{is_him}; + + my $short = $json->{txtS}; + my $text = $json->{txtN}; + my $type = $json->{type}; + my $code = $json->{code}; + my $prio = $json->{prio}; + + if ($is_him) { + $short = $json->{head}; + $text = $json->{text}; + $code = $json->{hid}; + } + + my $ref = { + short => $short, + text => $text, + type => $type, + code => $code, + prio => $prio, + is_him => $is_him, + ref_count => $conf{ref_count}, + }; + + bless( $ref, $obj ); + + return $ref; +} + +sub TO_JSON { + my ($self) = @_; + + return { %{$self} }; +} + +1; + +__END__ + +=head1 NAME + +Travel::Status::DE::HAFAS::Message - An arrival/departure-related message. + +=head1 SYNOPSIS + + if ($message->text) { + printf("%s: %s\n", $message->short, $message->text); + } + else { + say $message->short; + } + +=head1 VERSION + +version 6.03 + +=head1 DESCRIPTION + +Travel::Status::DE::HAFAS::Message describes a message belonging to an +arrival or departure. Messages may refer to planned schedule changes due to +construction work, the expected passenger volume, or similar. + +=head1 METHODS + +=head2 ACCESSORS + +=over + +=item $message->short + +Message header. May be a concise single-sentence summary or a mostly useless +string such as "Information". Does not contain newlines. + +=item $message->text + +Detailed message content. Does not contain newlines. + +=item $message->code + +Two-digit message code, seems to be only used with messages of type "A". +Details unknown. + +=item $message->type + +A single character indicating the message type. +The following types are known: + +=over + +=item A + +Generic information about a specific trip such as "WiFi available", "air +conditioning", "DB tickets are not valid here", or "from here on as [line] +towards [destination]". + +=item C + +"Current information available", "Journey cancelled", "connection may not be caught", possibly more. + +=item D + +Large-scale disruption, e.g. medical emergency on line. + +=item G + +Platform change, possibly more. + +=item H + +Misc stuff such as "Journey contains trains with mandatory reservation" or +"ICE Sprinter". + +=item L + +Replacement journey + +=item M + +Free-text infos about construction sites, broken elevators, large events and +similar occasions. + +=item P + +Journey has been cancelled, possibly more. + +=back + +=item $message->ref_count + +Counter indicating how often this message is used by the requested +arrivals/departures. ref_count is an integer between 1 and the number of +results. If ref_count is 1, it is referenced by a single result only. + +=item $message->is_him + +True if it is a HIM message (typically used for service information), false +if not (message may be a REM instead, indicating e.g. presence of a bicycle +carriage or WiFi). + +=back + +=head1 DIAGNOSTICS + +None. + +=head1 DEPENDENCIES + +=over + +=item Class::Accessor(3pm) + +=back + +=head1 BUGS AND LIMITATIONS + +None known. + +=head1 SEE ALSO + +Travel::Status::DE::HAFAS(3pm). + +=head1 AUTHOR + +Copyright (C) 2020-2023 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt> + +=head1 LICENSE + +This module is licensed under the same terms as Perl itself. diff --git a/lib/Travel/Status/DE/HAFAS/Polyline.pm b/lib/Travel/Status/DE/HAFAS/Polyline.pm new file mode 100644 index 0000000..d07844b --- /dev/null +++ b/lib/Travel/Status/DE/HAFAS/Polyline.pm @@ -0,0 +1,97 @@ +package Travel::Status::DE::HAFAS::Polyline; + +use strict; +use warnings; +use 5.014; + +# Adapted from code by Slaven Rezic +# +# Copyright (C) 2009,2010,2012,2017,2018 Slaven Rezic. All rights reserved. +# This package is free software; you can redistribute it and/or +# modify it under the same terms as Perl itself. +# +# Mail: slaven@rezic.de +# WWW: http://www.rezic.de/eserte/ + +use parent 'Exporter'; +our @EXPORT_OK = qw(decode_polyline); + +our $VERSION = '6.03'; + +# Translated this php script +# <http://unitstep.net/blog/2008/08/02/decoding-google-maps-encoded-polylines-using-php/> +# to perl +sub decode_polyline { + my ($encoded) = @_; + + my $length = length $encoded; + my $index = 0; + my @points; + my $lat = 0; + my $lng = 0; + + while ( $index < $length ) { + + # The encoded polyline consists of a latitude value followed + # by a longitude value. They should always come in pairs. Read + # the latitude value first. + for my $val ( \$lat, \$lng ) { + my $shift = 0; + my $result = 0; + + # Temporary variable to hold each ASCII byte. + my $b; + do { + # The `ord(substr($encoded, $index++))` statement returns + # the ASCII code for the character at $index. Subtract 63 + # to get the original value. (63 was added to ensure + # proper ASCII characters are displayed in the encoded + # polyline string, which is `human` readable) + $b = ord( substr( $encoded, $index++, 1 ) ) - 63; + + # AND the bits of the byte with 0x1f to get the original + # 5-bit `chunk. Then left shift the bits by the required + # amount, which increases by 5 bits each time. OR the + # value into $results, which sums up the individual 5-bit + # chunks into the original value. Since the 5-bit chunks + # were reversed in order during encoding, reading them in + # this way ensures proper summation. + $result |= ( $b & 0x1f ) << $shift; + $shift += 5; + } + + # Continue while the read byte is >= 0x20 since the last + # `chunk` was not OR'd with 0x20 during the conversion + # process. (Signals the end) + while ( $b >= 0x20 ); + + # see last paragraph of "Integer Arithmetic" in perlop.pod + use integer; + + # Check if negative, and convert. (All negative values have the last bit + # set) + my $dtmp + = ( ( $result & 1 ) ? ~( $result >> 1 ) : ( $result >> 1 ) ); + + # Compute actual latitude (resp. longitude) since value is + # offset from previous value. + $$val += $dtmp; + } + + # The actual latitude and longitude values were multiplied by + # 1e5 before encoding so that they could be converted to a 32-bit + # integer representation. (With a decimal accuracy of 5 places) + # Convert back to original values. + push( + @points, + { + lat => $lat * 1e-5, + lon => $lng * 1e-5 + } + ); + } + + return @points; +} + +1; diff --git a/lib/Travel/Status/DE/HAFAS/Product.pm b/lib/Travel/Status/DE/HAFAS/Product.pm new file mode 100644 index 0000000..cd85c16 --- /dev/null +++ b/lib/Travel/Status/DE/HAFAS/Product.pm @@ -0,0 +1,195 @@ +package Travel::Status::DE::HAFAS::Product; + +# vim:foldmethod=marker + +use strict; +use warnings; +use 5.014; + +use parent 'Class::Accessor'; + +our $VERSION = '6.03'; + +Travel::Status::DE::HAFAS::Product->mk_ro_accessors( + qw(class line_id line_no name number type type_long operator)); + +# {{{ Constructor + +sub new { + my ( $obj, %opt ) = @_; + + my $product = $opt{product}; + my $common = $opt{common}; + my $opL = $common->{opL}; + + # DB: + # catIn / catOutS eq "IXr" => "ICE X Regio"? regional tickets are generally accepted + # <= does not hold + + my $class = $product->{cls}; + my $name = $product->{addName} // $product->{name}; + my $line_no = $product->{prodCtx}{line}; + my $train_no = $product->{prodCtx}{num}; + my $cat = $product->{prodCtx}{catOut}; + my $catlong = $product->{prodCtx}{catOutL}; + + # ÖBB, you so silly + if ( $name and $name =~ m{Zug-Nr} and $product->{nameS} ) { + $name = $product->{nameS}; + } + + if ( $name and $cat and $name eq $cat and $product->{nameS} ) { + $name .= ' ' . $product->{nameS}; + } + + if ( defined $train_no and not $train_no ) { + $train_no = undef; + } + + if ( + not defined $line_no + and defined $product->{prodCtx}{matchId} + and + ( not defined $train_no or $product->{prodCtx}{matchId} ne $train_no ) + ) + { + $line_no = $product->{prodCtx}{matchId}; + } + + my $line_id; + if ( $product->{prodCtx}{lineId} ) { + $line_id = lc( $product->{prodCtx}{lineId} =~ s{_+}{-}gr ); + } + + my $operator; + if ( defined $product->{oprX} ) { + if ( my $opref = $opL->[ $product->{oprX} ] ) { + $operator = $opref->{name}; + } + } + + my $ref = { + name => $name, + number => $train_no, + line_id => $line_id, + line_no => $line_no, + type => $cat, + type_long => $catlong, + class => $class, + operator => $operator, + }; + + bless( $ref, $obj ); + + return $ref; +} + +# }}} + +sub TO_JSON { + my ($self) = @_; + + return { %{$self} }; +} + +1; + +__END__ + +=head1 NAME + +Travel::Status::DE::HAFAS::Product - Information about a HAFAS product +associated with a journey. + +=head1 SYNOPSIS + +=head1 VERSION + +version 6.03 + +=head1 DESCRIPTION + +Travel::Status::DE::HAFAS::Product describes a product (e.g. train or bus) +associated with a Travel::Status::DE::HAFAS::Journey(3pm) or one of its +stops. + +=head1 METHODS + +=head2 ACCESSORS + +=over + +=item $product->class + +An integer identifying the the mode of transport class. Semantics depend on +backend See Travel::Status::DE::HAFAS(3pm)'s C<< $hafas->get_active_service >> +method. + +=item $product->line_id + +Line identifier, or undef if it is unknown. +This is a backend-specific identifier, e.g. "7-vrr010-17" for VRR U17. +The format is compatible with L<https://github.com/Traewelling/line-colors>. + +=item $product->line_no + +Line number, or undef if it is unknown. +The line identifier may be a single number such as "11" (underground train +line U 11), a single word (e.g. "AIR") or a combination (e.g. "SB16"). +May also provide line numbers of IC/ICE services. + +=item $product->name + +Trip or line name, either in a format like "Bus SB16" (Bus line +SB16), "RE 42" (RegionalExpress train 42) or "IC 2901" (InterCity train 2901, +no line information). May contain extraneous whitespace characters. Note that +this accessor does not return line information for DB IC/ICE/EC services, even +if it is available. Use B<line_no> for those. + +=item $product->number + +Trip number (e.g. train number), or undef if it is unknown. + +=item $product->type + +Type of this product, e.g. "S" for S-Bahn, "RE" for Regional Express +or "STR" for tram / StraE<szlig>enbahn. + +=item $product->type_long + +Long type of this product, e.g. "S-Bahn" or "Regional-Express". + +=item $product->operator + +The operator responsible for this product. Returns undef +if the backend does not provide an operator. + +=back + +=head1 DIAGNOSTICS + +None. + +=head1 DEPENDENCIES + +=over + +=item Class::Accessor(3pm) + +=back + +=head1 BUGS AND LIMITATIONS + +None known. + +=head1 SEE ALSO + +Travel::Status::DE::HAFAS(3pm). + +=head1 AUTHOR + +Copyright (C) 2024 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt> + +=head1 LICENSE + +This module is licensed under the same terms as Perl itself. diff --git a/lib/Travel/Status/DE/HAFAS/Result.pm b/lib/Travel/Status/DE/HAFAS/Result.pm deleted file mode 100644 index 476f6fd..0000000 --- a/lib/Travel/Status/DE/HAFAS/Result.pm +++ /dev/null @@ -1,319 +0,0 @@ -package Travel::Status::DE::HAFAS::Result; - -use strict; -use warnings; -use 5.014; - -no if $] >= 5.018, warnings => 'experimental::smartmatch'; - -use parent 'Class::Accessor'; - -our $VERSION = '2.03'; - -Travel::Status::DE::HAFAS::Result->mk_ro_accessors( - qw(date datetime info raw_e_delay raw_delay time train route_end)); - -sub new { - my ( $obj, %conf ) = @_; - - my $ref = \%conf; - - return bless( $ref, $obj ); -} - -sub countdown { - my ($self) = @_; - - $self->{countdown} - //= $self->datetime->subtract_datetime( $self->{datetime_now} ) - ->in_units('minutes'); - - return $self->{countdown}; -} - -sub countdown_sec { - my ($self) = @_; - - $self->{countdown_sec} - //= $self->datetime->subtract_datetime( $self->{datetime_now} ) - ->in_units('seconds'); - - return $self->{countdown_sec}; -} - -sub delay { - my ($self) = @_; - - if ( defined $self->{raw_e_delay} ) { - return $self->{raw_e_delay}; - } - if ( defined $self->{raw_delay} - and $self->{raw_delay} ne q{-} - and $self->{raw_delay} ne 'cancel' ) - { - return $self->{raw_delay}; - } - return; -} - -sub destination { - my ($self) = @_; - - return $self->{route_end}; -} - -sub line { - my ($self) = @_; - - return $self->{train}; -} - -sub is_cancelled { - my ($self) = @_; - - if ( $self->{raw_delay} and $self->{raw_delay} eq 'cancel' ) { - return 1; - } - return 0; -} - -sub is_changed_platform { - my ($self) = @_; - - if ( defined $self->{new_platform} and defined $self->{platform} ) { - if ( $self->{new_platform} ne $self->{platform} ) { - return 1; - } - return 0; - } - if ( defined $self->{net_platform} ) { - return 1; - } - - return 0; -} - -sub messages { - my ($self) = @_; - - if ( $self->{messages} ) { - return @{ $self->{messages} }; - } - return; -} - -sub origin { - my ($self) = @_; - - return $self->{route_end}; -} - -sub platform { - my ($self) = @_; - - return $self->{new_platform} // $self->{platform}; -} - -sub TO_JSON { - my ($self) = @_; - - return { %{$self} }; -} - -sub type { - my ($self) = @_; - my $type; - - # $self->{train} is either "TYPE 12345" or "TYPE12345" - if ( $self->{train} =~ m{ \s }x ) { - ($type) = ( $self->{train} =~ m{ ^ ([^[:space:]]+) }x ); - } - else { - ($type) = ( $self->{train} =~ m{ ^ ([[:alpha:]]+) }x ); - } - - return $type; -} - -sub line_no { - my ($self) = @_; - my $line_no; - - # $self->{train} is either "TYPE 12345" or "TYPE12345" - if ( $self->{train} =~ m{ \s }x ) { - ($line_no) = ( $self->{train} =~ m{ ([^[:space:]]+) $ }x ); - } - else { - ($line_no) = ( $self->{train} =~ m{ ([[:digit:]]+) $ }x ); - } - - return $line_no; -} - -sub train_no { - my ($self) = @_; - - return $self->line_no; -} - -1; - -__END__ - -=head1 NAME - -Travel::Status::DE::HAFAS::Result - Information about a single -arrival/departure received by Travel::Status::DE::HAFAS - -=head1 SYNOPSIS - - for my $departure ($status->results) { - printf( - "At %s: %s to %s from platform %s\n", - $departure->time, - $departure->line, - $departure->destination, - $departure->platform, - ); - } - - # or (depending on module setup) - for my $arrival ($status->results) { - printf( - "At %s: %s from %s on platform %s\n", - $arrival->time, - $arrival->line, - $arrival->origin, - $arrival->platform, - ); - } - -=head1 VERSION - -version 2.03 - -=head1 DESCRIPTION - -Travel::Status::DE::HAFAS::Result describes a single arrival/departure -as obtained by Travel::Status::DE::HAFAS. It contains information about -the platform, time, route and more. - -=head1 METHODS - -=head2 ACCESSORS - -=over - -=item $result->countdown - -Difference between the time Travel::Status::DE::HAFAS->results -was called first and the arrival/departure time, in minutes. - -=item $result->countdown_sec - -Difference between the time Travel::Status::DE::HAFAS->results -was called first and the arrival/departure time, in seconds. - -=item $result->date - -Arrival/Departure date in "dd.mm.yyyy" format. - -=item $result->datetime - -DateTime object holding the arrival/departure date and time. - -=item $result->delay - -Returns the delay in minutes, or undef if it is unknown. -Also returns undef if the arrival/departure has been cancelled. - -=item $result->info - -Returns additional information, for instance the most recent delay reason. -undef if no (useful) information is available. - -=item $result->is_cancelled - -True if the arrival/departure was cancelled, false otherwise. - -=item $result->is_changed_platform - -True if the platform (as returned by the B<platform> accessor) is not the -scheduled one. Note that the scheduled platform is unknown in this case. - -=item $result->messages - -Returns a list of message strings related to this result. Messages usually are -service notices (e.g. "missing carriage") or detailed delay reasons -(e.g. "switch damage between X and Y, expect delays"). - -=item $result->line - -=item $result->train - -Returns the line name, either in a format like "Bus SB16" (Bus line SB16) -or "RE 10111" (RegionalExpress train 10111, no line information). -May contain extraneous whitespace characters. - -=item $result->line_no - -=item $result->train_no - -Returns the line/train number, for instance "SB16" (bus line SB16), -"11" (Underground train line U 11) or 1011 ("RegionalExpress train 1011"). -Note that this may not be a number at all: Some transport services also -use single-letter characters or words (e.g. "AIR") as line numbers. - -=item $result->platform - -Returns the arrival/departure platform. -Realtime data if available, schedule data otherwise. - -=item $result->route_end - -=item $result->destination - -=item $result->origin - -Returns the last element of the route. Depending on how you set up -Travel::Status::DE::HAFAS (arrival or departure listing), this is -either the result's destination or its origin station. - -=item $result->time - -Returns the arrival/departure time as string in "hh:mm" format. - -=item $result->type - -Returns the type of this result, e.g. "S" for S-Bahn, "RE" for Regional Express -or "STR" for tram / StraE<szlig>enbahn. - -=back - -=head1 DIAGNOSTICS - -None. - -=head1 DEPENDENCIES - -=over - -=item Class::Accessor(3pm) - -=back - -=head1 BUGS AND LIMITATIONS - -None known. - -=head1 SEE ALSO - -Travel::Status::DE::HAFAS(3pm). - -=head1 AUTHOR - -Copyright (C) 2015-2017 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt> - -=head1 LICENSE - -This module is licensed under the same terms as Perl itself. diff --git a/lib/Travel/Status/DE/HAFAS/Stop.pm b/lib/Travel/Status/DE/HAFAS/Stop.pm new file mode 100644 index 0000000..98af9ed --- /dev/null +++ b/lib/Travel/Status/DE/HAFAS/Stop.pm @@ -0,0 +1,368 @@ +package Travel::Status::DE::HAFAS::Stop; + +# vim:foldmethod=marker + +use strict; +use warnings; +use 5.014; + +use parent 'Class::Accessor'; + +our $VERSION = '6.03'; + +Travel::Status::DE::HAFAS::Stop->mk_ro_accessors( + qw(loc + rt_arr sched_arr arr arr_delay arr_cancelled prod_arr + rt_dep sched_dep dep dep_delay dep_cancelled prod_dep + delay direction + rt_platform sched_platform platform is_changed_platform + is_additional tz_offset + load + ) +); + +# {{{ Constructor + +sub new { + my ( $obj, %opt ) = @_; + + my $stop = $opt{stop}; + my $common = $opt{common}; + my $prodL = $opt{prodL}; + my $date = $opt{date}; + my $datetime_ref = $opt{datetime_ref}; + my $hafas = $opt{hafas}; + my $strp_obj = $opt{hafas}{strptime_obj}; + + my $prod_arr + = defined $stop->{aProdX} ? $prodL->[ $stop->{aProdX} ] : undef; + my $prod_dep + = defined $stop->{dProdX} ? $prodL->[ $stop->{dProdX} ] : undef; + + # dIn. / aOut. -> may passengers enter / exit the train? + + my $sched_platform = $stop->{aPlatfS} // $stop->{dPlatfS}; + my $rt_platform = $stop->{aPlatfR} // $stop->{dPlatfR}; + my $changed_platform = $stop->{aPlatfCh} // $stop->{dPlatfCh}; + + my $arr_cancelled = $stop->{aCncl}; + my $dep_cancelled = $stop->{dCncl}; + my $is_additional = $stop->{isAdd}; + + my $ref = { + loc => $opt{loc}, + direction => $stop->{dDirTxt}, + sched_platform => $sched_platform, + rt_platform => $rt_platform, + is_changed_platform => $changed_platform, + platform => $rt_platform // $sched_platform, + arr_cancelled => $arr_cancelled, + dep_cancelled => $dep_cancelled, + is_additional => $is_additional, + prod_arr => $prod_arr, + prod_dep => $prod_dep, + }; + + bless( $ref, $obj ); + + my $sched_arr = $ref->handle_day_change( + input => $stop->{aTimeS}, + offset => $stop->{aTZOffset}, + date => $date, + strp_obj => $strp_obj, + ref => $datetime_ref + ); + + my $rt_arr = $ref->handle_day_change( + input => $stop->{aTimeR}, + offset => $stop->{aTZOffset}, + date => $date, + strp_obj => $strp_obj, + ref => $datetime_ref + ); + + my $sched_dep = $ref->handle_day_change( + input => $stop->{dTimeS}, + offset => $stop->{dTZOffset}, + date => $date, + strp_obj => $strp_obj, + ref => $datetime_ref + ); + + my $rt_dep = $ref->handle_day_change( + input => $stop->{dTimeR}, + offset => $stop->{dTZOffset}, + date => $date, + strp_obj => $strp_obj, + ref => $datetime_ref + ); + + $ref->{arr_delay} + = ( $sched_arr and $rt_arr ) + ? ( $rt_arr->epoch - $sched_arr->epoch ) / 60 + : undef; + + $ref->{dep_delay} + = ( $sched_dep and $rt_dep ) + ? ( $rt_dep->epoch - $sched_dep->epoch ) / 60 + : undef; + + $ref->{delay} = $ref->{dep_delay} // $ref->{arr_delay}; + + $ref->{sched_arr} = $sched_arr; + $ref->{sched_dep} = $sched_dep; + $ref->{rt_arr} = $rt_arr; + $ref->{rt_dep} = $rt_dep; + $ref->{arr} = $rt_arr // $sched_arr; + $ref->{dep} = $rt_dep // $sched_dep; + + my @messages; + for my $msg ( @{ $stop->{msgL} // [] } ) { + if ( $msg->{type} eq 'REM' and defined $msg->{remX} ) { + push( @messages, + $hafas->add_message( $opt{common}{remL}[ $msg->{remX} ] ) ); + } + elsif ( $msg->{type} eq 'HIM' and defined $msg->{himX} ) { + push( @messages, + $hafas->add_message( $opt{common}{himL}[ $msg->{himX} ], 1 ) ); + } + else { + say "Unknown message type $msg->{type}"; + } + } + $ref->{messages} = \@messages; + + $ref->{load} = {}; + for my $tco_id ( @{ $stop->{dTrnCmpSX}{tcocX} // [] } ) { + my $tco_kv = $common->{tcocL}[$tco_id]; + $ref->{load}{ $tco_kv->{c} } = $tco_kv->{r}; + } + + return $ref; +} + +# }}} + +sub handle_day_change { + my ( $self, %opt ) = @_; + my $date = $opt{date}; + my $timestr = $opt{input}; + my $offset = $opt{offset}; + + if ( not defined $timestr ) { + return; + } + + if ( length($timestr) == 8 ) { + + # arrival time includes a day offset + my $offset_date = $opt{ref}->clone; + $offset_date->add( days => substr( $timestr, 0, 2, q{} ) ); + $offset_date = $offset_date->strftime('%Y%m%d'); + $timestr = $opt{strp_obj}->parse_datetime("${offset_date}T${timestr}"); + } + else { + $timestr = $opt{strp_obj}->parse_datetime("${date}T${timestr}"); + } + + if ( defined $offset and $offset != $timestr->offset / 60 ) { + $self->{tz_offset} = $offset - $timestr->offset / 60; + $timestr->subtract( minutes => $self->{tz_offset} ); + } + + return $timestr; +} + +sub messages { + my ($self) = @_; + + if ( $self->{messages} ) { + return @{ $self->{messages} }; + } + return; +} + +sub TO_JSON { + my ($self) = @_; + + my $ret = { %{$self} }; + + for my $k ( keys %{$ret} ) { + if ( ref( $ret->{$k} ) eq 'DateTime' ) { + $ret->{$k} = $ret->{$k}->epoch; + } + } + + return $ret; +} + +1; + +__END__ + +=head1 NAME + +Travel::Status::DE::HAFAS::Stop - Information about a HAFAS stop. + +=head1 SYNOPSIS + + # in journey mode + for my $stop ($journey->route) { + printf( + %5s -> %5s %s\n", + $stop->arr ? $stop->arr->strftime('%H:%M') : '--:--', + $stop->dep ? $stop->dep->strftime('%H:%M') : '--:--', + $stop->loc->name + ); + } + +=head1 VERSION + +version 6.03 + +=head1 DESCRIPTION + +Travel::Status::DE::HAFAS::Stop describes a +Travel::Status::DE::HAFAS::Journey(3pm)'s stop at a given +Travel::Status::DE::HAFAS::Location(3pm) with arrival/departure time, +platform, etc. + +All date and time entries refer to the backend time zone (Europe/Berlin in most +cases) and do not take local time into account; see B<tz_offset> for the +latter. + +=head1 METHODS + +=head2 ACCESSORS + +=over + +=item $stop->loc + +Travel::Status::DE::HAFAS::Location(3pm) instance describing stop name, EVA +ID, et cetera. + +=item $stop->rt_arr + +DateTime object for actual arrival. + +=item $stop->sched_arr + +DateTime object for scheduled arrival. + +=item $stop->arr + +DateTime object for actual or scheduled arrival. + +=item $stop->arr_delay + +Arrival delay in minutes. + +=item $stop->arr_cancelled + +Arrival is cancelled. + +=item $stop->rt_dep + +DateTime object for actual departure. + +=item $stop->sched_dep + +DateTime object for scheduled departure. + +=item $stop->dep + +DateTIme object for actual or scheduled departure. + +=item $stop->dep_delay + +Departure delay in minutes. + +=item $stop->dep_cancelled + +Departure is cancelled. + +=item $stop->tz_offset + +Offset between backend time zone (default: Europe/Berlin) and this stop's time +zone in minutes, if any. For instance, if the backend uses UTC+2 (CEST) and the +stop uses UTC+1 (IST), tz_offset is -60. Returns undef if both use the same +time zone (or rather, the same UTC offset). + +=item $stop->delay + +Departure or arrival delay in minutes. + +=item $stop->direction + +Direction signage from this stop on, undef if unchanged. + +=item $stop->messages + +List of Travel::Status::DE::HAFAS::Message(3pm) instances related to this stop. +These typically refer to delay reasons, platform changes, or changes in the +line number / direction heading. + +=item $stop->prod_arr + +Travel::Status::DE::HAFAS::Product(3pm) instance describing the transit product +(name, type, line number, operator, ...) upon arrival at this stop. + +=item $stop->prod_dep + +Travel::Status::DE::HAFAS::Product(3pm) instance describing the transit product +(name, type, line number, operator, ...) upon departure from this stop. + +=item $stop->rt_platform + +Actual platform. + +=item $stop->sched_platform + +Scheduled platform. + +=item $stop->platform + +Actual or scheduled platform. + +=item $stop->is_changed_platform + +True if real-time and scheduled platform disagree. + +=item $stop->is_additional + +True if the stop is an unscheduled addition to the train's route. + +=item $stop->load + +Expected utilization / passenger load from this stop on. + +=back + +=head1 DIAGNOSTICS + +None. + +=head1 DEPENDENCIES + +=over + +=item Class::Accessor(3pm) + +=back + +=head1 BUGS AND LIMITATIONS + +None known. + +=head1 SEE ALSO + +Travel::Status::DE::HAFAS(3pm). + +=head1 AUTHOR + +Copyright (C) 2023 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt> + +=head1 LICENSE + +This module is licensed under the same terms as Perl itself. diff --git a/lib/Travel/Status/DE/HAFAS/StopFinder.pm b/lib/Travel/Status/DE/HAFAS/StopFinder.pm index ed575da..10f48da 100644 --- a/lib/Travel/Status/DE/HAFAS/StopFinder.pm +++ b/lib/Travel/Status/DE/HAFAS/StopFinder.pm @@ -5,22 +5,22 @@ use warnings; use 5.014; use utf8; -no if $] >= 5.018, warnings => 'experimental::smartmatch'; - -use Carp qw(confess); +use Carp qw(confess); use Encode qw(decode); use JSON; use LWP::UserAgent; -our $VERSION = '2.03'; +our $VERSION = '6.03'; + +# {{{ Constructors sub new { my ( $obj, %conf ) = @_; my $lang = $conf{language} // 'd'; - my $ua = $conf{ua}; + my $ua = $conf{ua}; - if ( not $ua ) { + if ( not $ua and not $conf{async} ) { my %lwp_options = %{ $conf{lwp_options} // { timeout => 10 } }; $ua = LWP::UserAgent->new(%lwp_options); $ua->env_proxy; @@ -46,6 +46,10 @@ sub new { bless( $ref, $obj ); + if ( $conf{async} ) { + return $ref; + } + my $url = $conf{url} . "/${lang}n"; $reply = $ua->post( $url, $ref->{post} ); @@ -69,6 +73,59 @@ sub new { return $ref; } +sub new_p { + my ( $obj, %conf ) = @_; + my $promise = $conf{promise}->new; + + if ( not $conf{input} ) { + return $promise->reject('You need to specify an input value'); + } + if ( not $conf{url} ) { + return $promise->reject('You need to specify a URL'); + } + + my $self = $obj->new( %conf, async => 1 ); + $self->{promise} = $conf{promise}; + + my $lang = $conf{language} // 'd'; + my $url = $conf{url} . "/${lang}n"; + $conf{user_agent}->post_p( $url, form => $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; + + $self->{raw_reply} = $content; + + $self->{raw_reply} =~ s{ ^ SLs [.] sls = }{}x; + $self->{raw_reply} =~ s{ ; SLs [.] showSuggestion [(] [)] ; $ }{}x; + + if ( $self->{developer_mode} ) { + say $self->{raw_reply}; + } + + $self->{json} = from_json( $self->{raw_reply} ); + + $promise->resolve( $self->results ); + return; + } + )->catch( + sub { + my ($err) = @_; + $promise->reject($err); + return; + } + )->wait; + + return $promise; +} + +# }}} + sub errstr { my ($self) = @_; @@ -109,7 +166,7 @@ finder services use Travel::Status::DE::HAFAS::StopFinder; my $sf = Travel::Status::DE::HAFAS::StopFinder->new( - url => 'http://reiseauskunft.bahn.de/bin/ajax-getstop.exe', + url => 'https://reiseauskunft.bahn.de/bin/ajax-getstop.exe', input => 'Borbeck', ); @@ -123,17 +180,21 @@ finder services =head1 VERSION -version 2.03 +version 6.03 =head1 DESCRIPTION Travel::Status::DE::HAFAS::StopFinder is an interface to the stop finder service of HAFAS based arrival/departure monitors, for instance the one -available at L<http://reiseauskunft.bahn.de/bin/ajax-getstop.exe/dn>. +available at L<https://reiseauskunft.bahn.de/bin/ajax-getstop.exe/dn>. It takes a string (usually a location or station name) and reports all stations and stops which are lexically similar to it. +StopFinder typically gives less coarse results than +Travel::Status::DE::HAFAS(3pm)'s locationSearch method. However, it is unclear +whether HAFAS instances will continue supporting it in the future. + =head1 METHODS =over @@ -172,18 +233,39 @@ you can use an empty hashref to override it. =back -=item $status->errstr +=item my $stopfinder_p = Travel::Status::DE::HAFAS::StopFinder->new_p(I<%opt>) + +Return a promise that resolves into a list of +Travel::Status::DE::HAFAS::StopFinder results ($stopfinder->results) on success +and rejects with an error message ($stopfinder->errstr) on failure. In addition +to the arguments of B<new>, the following mandatory arguments must be set. + +=over + +=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 + +=item $stopfinder->errstr In case of an error in the HTTP request, returns a string describing it. If no error occurred, returns undef. -=item $status->results +=item $stopfinder->results Returns a list of stop candidates. Each list element is a hash reference. The -hash keys are B<id> (IBNR / UIC station code) and B<name> (stop name). Both can -be used as input for the Travel::Status::DE::HAFAS(3pm) constructor. +hash keys are B<id> (IBNR / EVA / UIC station code) and B<name> (stop name). +Both can be used as input for the Travel::Status::DE::HAFAS(3pm) constructor. -If no matching results were found or the parser / http request failed, returns +If no matching results were found or the parser / HTTP request failed, returns the empty list. =back @@ -212,7 +294,7 @@ Travel::Status::DE::HAFAS(3pm). =head1 AUTHOR -Copyright (C) 2015-2017 by Daniel Friesel E<lt>derf@finalrewind.orgE<gt> +Copyright (C) 2015-2023 by Birte Kristina Friesel E<lt>derf@finalrewind.orgE<gt> =head1 LICENSE diff --git a/scripts/check-hafas-urls b/scripts/check-hafas-urls new file mode 100755 index 0000000..911faad --- /dev/null +++ b/scripts/check-hafas-urls @@ -0,0 +1,22 @@ +#!/usr/bin/env zsh + +export PERL5LIB=lib + +checks="AVV Ponttor, AC +BART San Francisco International Airport, San Mateo +DB Berlin Jannowitzbrücke +IE Dublin +NAHSH Flensburg +NASA Wernigerode Hbf +NVV Kassel Hauptbahnhof +ÖBB Wien Meidling +VBB S+U Jannowitzbrücke (Berlin) +VBN Diepholz" + +echo $checks | while read service stop; do + echo -n "${service} ... " + if bin/hafas-m -s $service $stop > /dev/null; then + echo OK + fi +done + diff --git a/scripts/makedeb-docker b/scripts/makedeb-docker new file mode 100755 index 0000000..6c06971 --- /dev/null +++ b/scripts/makedeb-docker @@ -0,0 +1,11 @@ +#!/bin/sh + +mkdir -p out + +docker run --rm -v "${PWD}:/orig:ro" -v "${PWD}/scripts:/scripts:ro" \ + -v "${PWD}/out:/out" -e USER=$(id -u) -e GROUP=$(id -g) \ + -e "DEBEMAIL=${DEBEMAIL}" -e "DEBFULLNAME=${DEBFULLNAME}" \ + -e "LOGNAME=${LOGNAME}" -e "VERSION=$(git describe --dirty)-1" \ + debian:buster /scripts/makedeb-docker-helper + +echo "Debian package has been written to $(pwd)/out" diff --git a/scripts/makedeb-docker-helper b/scripts/makedeb-docker-helper new file mode 100755 index 0000000..e15f31d --- /dev/null +++ b/scripts/makedeb-docker-helper @@ -0,0 +1,34 @@ +#!/bin/sh + +set -e + +export DEBIAN_FRONTEND=noninteractive +export APT_LISTCHANGES_FRONTEND=none + +apt-get update +apt-get -y install \ + apt-file dh-make-perl libmodule-build-perl \ + libclass-accessor-perl libdatetime-perl libdatetime-format-strptime-perl \ + liblwp-protocol-https-perl libjson-perl libjson-xs-perl \ + liblist-moreutils-perl \ + libwww-perl \ + libtest-compile-perl libtest-pod-perl \ + libtest-simple-perl + +apt-file update +apt-cache dumpavail | dpkg --merge-avail + +mkdir -p /src/app +cp -a /orig/Build.PL /orig/Changelog /orig/README.md /src/app +cp -a /orig/bin /orig/lib /src/app +cd /src/app + +sed -i 's/sign *=> *1/sign => 0/' Build.PL +perl Build.PL +perl Build +perl Build manifest +perl Build dist +mv Travel-Status-DE-DeutscheBahn-*.tar.gz ../app.tar.gz +dh-make-perl --build --version "${VERSION}" +chown ${USER}:${GROUP} ../*.deb +mv -v ../*.deb /out diff --git a/scripts/update-readme b/scripts/update-readme new file mode 100755 index 0000000..27ea318 --- /dev/null +++ b/scripts/update-readme @@ -0,0 +1,20 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use 5.010; + +use File::Slurp qw(read_file write_file); +use Travel::Status::DE::HAFAS; + +my $service_list = q{}; + +for my $s ( Travel::Status::DE::HAFAS::get_services() ) { + $service_list .= sprintf( "* [%s](%s)\n", $s->{name}, $s->{url} ); +} + +my $readme = read_file('README.md', { binmode => ':utf8' } ); + +$readme + =~ s{(?<=to URLs not listed here.\n\n).*(?=\nSee the \[)}{$service_list}s; + +write_file('README.md', {binmode => ':utf8'}, $readme); diff --git a/t/20-db.t b/t/20-db.t new file mode 100755 index 0000000..6dd7760 --- /dev/null +++ b/t/20-db.t @@ -0,0 +1,368 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use 5.020; + +use utf8; + +use File::Slurp qw(read_file); +use JSON; +use Test::More tests => 106; + +use Travel::Status::DE::HAFAS; + +my $json + = JSON->new->utf8->decode( read_file('t/in/DB.Berlin Jannowitzbrücke.json') ); + +my $status = Travel::Status::DE::HAFAS->new( + service => 'DB', + station => 'Berlin Jannowitzbrücke', + json => $json +); + +is( $status->errcode, undef, 'no error code' ); +is( $status->errstr, undef, 'no error string' ); + +is( + $status->get_active_service->{name}, + 'Deutsche Bahn', + 'active service name' +); + +is( scalar $status->results, 30, 'number of results' ); + +my @results = $status->results; + +# Result 0: Bus + +is( + $results[0]->datetime->strftime('%Y%m%d %H%M%S'), + '20221002 170500', + 'result 0: datetime' +); +is( $results[0]->delay, 10, 'result 0: delay' ); +ok( !$results[0]->is_cancelled, 'result 0: not cancelled' ); +ok( !$results[0]->is_changed_platform, 'result 0: platform not changed' ); + +is( $results[0]->name, 'Bus 300', 'result 0: name' ); +is( $results[0]->type, 'Bus', 'result 0: type' ); +is( $results[0]->type_long, 'Bus', 'result 0: type_long' ); +is( $results[0]->class, 32, 'result 0: class' ); +is( $results[0]->line, 'Bus 300', 'result 0: line' ); +is( $results[0]->line_no, '300', 'result 0: line' ); +is( $results[0]->number, '50833', 'result 0: number' ); + +is( $results[0]->operator, 'Nahreisezug', 'result 0: operator' ); +is( $results[0]->platform, undef, 'result 0: platform' ); + +is( $results[0]->direction, 'Tiergarten, Philharmonie', 'result 0: direction' ); + +for my $res ( $results[0]->route_end, $results[0]->destination ) { + is( $res, 'Philharmonie Süd, Berlin', 'result 0: route start/end' ); +} + +is( scalar $results[0]->route_interesting, + 3, 'result 0: route_interesting: 3 elements' ); +is( + ( $results[0]->route_interesting )[0]->loc->name, + 'Alexanderstr., Berlin', + 'result 0: route_interesting 0: name' +); +is( + ( $results[0]->route_interesting )[1]->loc->name, + 'Alexanderplatz (S+U)/Grunerstr., Berlin', + 'result 0: route_interesting 1: name' +); +is( + ( $results[0]->route_interesting )[2]->loc->name, + 'Rotes Rathaus (U), Berlin', + 'result 0: route_interesting 2: name' +); + +is( scalar $results[0]->route, 12, 'result 0: route: 12 elements' ); +is( + ( $results[0]->route )[0]->loc->name, + 'Alexanderstr., Berlin', + 'result 0: route 0: name' +); +is( + ( $results[0]->route )[1]->loc->name, + 'Alexanderplatz (S+U)/Grunerstr., Berlin', + 'result 0: route 1: name' +); +is( + ( $results[0]->route )[2]->loc->name, + 'Rotes Rathaus (U), Berlin', + 'result 0: route 2: name' +); +is( + ( $results[0]->route )[3]->loc->name, + 'Museumsinsel (U), Berlin', + 'result 0: route 3: name' +); +is( + ( $results[0]->route )[4]->loc->name, + 'Staatsoper, Berlin', + 'result 0: route 4: name' +); +is( + ( $results[0]->route )[5]->loc->name, + 'Unter den Linden (U), Berlin', + 'result 0: route 5: name' +); +is( + ( $results[0]->route )[6]->loc->name, + 'Behrenstr./Wilhelmstr., Berlin', + 'result 0: route 6: name' +); +is( + ( $results[0]->route )[7]->loc->name, + 'Mohrenstr. (U), Berlin', + 'result 0: route 7: name' +); +is( + ( $results[0]->route )[8]->loc->name, + 'Leipziger Str./Wilhelmstr., Berlin', + 'result 0: route 8: name' +); +is( + ( $results[0]->route )[9]->loc->name, + 'Potsdamer Platz [Bus Leipziger Str.] (S+U), Berlin', + 'result 0: route 9: name' +); +is( + ( $results[0]->route )[10]->loc->name, + 'Varian-Fry-Str./Potsdamer Platz, Berlin', + 'result 0: route 10: name' +); +is( + ( $results[0]->route )[11]->loc->name, + 'Philharmonie Süd, Berlin', + 'result 0: route 11: name' +); + +is( + $results[0]->sched_datetime->strftime('%Y%m%d %H%M%S'), + '20221002 165500', + 'result 0: sched_datetime' +); + +# Result 2: U-Bahn + +is( + $results[2]->datetime->strftime('%Y%m%d %H%M%S'), + '20221002 170000', + 'result 2: datetime' +); +is( $results[2]->delay, 0, 'result 2: delay' ); +ok( !$results[2]->is_cancelled, 'result 2: not cancelled' ); +ok( !$results[2]->is_changed_platform, 'result 2: platform not changed' ); + +is( $results[2]->name, 'U 8', 'result 2: name' ); +is( $results[2]->type, 'U', 'result 2: type' ); +is( $results[2]->type_long, 'U-Bahn', 'result 2: type_long' ); +is( $results[2]->class, 128, 'result 2: class' ); +is( $results[2]->line, 'U 8', 'result 2: line' ); +is( $results[2]->line_no, '8', 'result 2: line' ); +is( $results[2]->number, '20024', 'result 2: number' ); + +is( $results[2]->operator, 'Nahreisezug', 'result 2: operator' ); +is( $results[2]->platform, undef, 'result 2: no platform' ); + +is( $results[2]->direction, 'Hermannstr. (S+U), Berlin', + 'result 2: direction' ); + +for my $res ( $results[2]->route_end, $results[2]->destination ) { + is( $res, 'Hermannstr. (S+U), Berlin', 'result 2: route start/end' ); +} + +is( scalar $results[2]->route_interesting, + 3, 'result 2: route_interesting: 3 elements' ); +is( + ( $results[2]->route_interesting )[0]->loc->name, + 'Heinrich-Heine-Str. (U), Berlin', + 'result 2: route_interesting 0: name' +); +is( + ( $results[2]->route_interesting )[1]->loc->name, + 'Moritzplatz (U), Berlin', + 'result 2: route_interesting 1: name' +); +is( + ( $results[2]->route_interesting )[2]->loc->name, + 'Kottbusser Tor (U), Berlin', + 'result 2: route_interesting 2: name' +); + +is( scalar $results[2]->route, 8, 'result 2: route: 8 elements' ); +is( + ( $results[2]->route )[0]->loc->name, + 'Heinrich-Heine-Str. (U), Berlin', + 'result 2: route 0: name' +); +is( + ( $results[2]->route )[1]->loc->name, + 'Moritzplatz (U), Berlin', + 'result 2: route 1: name' +); +is( + ( $results[2]->route )[2]->loc->name, + 'Kottbusser Tor (U), Berlin', + 'result 2: route 2: name' +); +is( + ( $results[2]->route )[3]->loc->name, + 'Schönleinstr. (U), Berlin', + 'result 2: route 3: name' +); +is( + ( $results[2]->route )[4]->loc->name, + 'Hermannplatz (U), Berlin', + 'result 2: route 4: name' +); +is( + ( $results[2]->route )[5]->loc->name, + 'Boddinstr. (U), Berlin', + 'result 2: route 5: name' +); +is( + ( $results[2]->route )[6]->loc->name, + 'Leinestr. (U), Berlin', + 'result 2: route 6: name' +); +is( + ( $results[2]->route )[7]->loc->name, + 'Hermannstr. (S+U), Berlin', + 'result 2: route 7: name' +); + +is( + $results[2]->sched_datetime->strftime('%Y%m%d %H%M%S'), + '20221002 170000', + 'result 2: sched_datetime' +); + +# Result 3: S-Bahn + +is( + $results[3]->datetime->strftime('%Y%m%d %H%M%S'), + '20221002 170100', + 'result 3: datetime' +); +is( $results[3]->delay, 0, 'result 3: delay' ); +ok( !$results[3]->is_cancelled, 'result 3: not cancelled' ); +ok( !$results[3]->is_changed_platform, 'result 3: platform not changed' ); + +is( $results[3]->name, 'S 3', 'result 3: name' ); +is( $results[3]->type, 'S', 'result 3: type' ); +is( $results[3]->type_long, 'S-Bahn', 'result 3: type_long' ); +is( $results[0]->class, 32, 'result 3: class' ); +is( $results[3]->line, 'S 3', 'result 3: line' ); +is( $results[3]->line_no, '3', 'result 3: line' ); +is( $results[3]->number, '3122', 'result 3: number' ); + +is( $results[3]->operator, 'S-Bahn Berlin', 'result 3: operator' ); +is( $results[3]->platform, 4, 'result 3: platform' ); + +is( $results[3]->direction, 'Berlin-Spandau (S)', 'result 3: direction' ); + +for my $res ( $results[3]->route_end, $results[3]->destination ) { + is( $res, 'Berlin-Spandau (S)', 'result 3: route start/end' ); +} + +is( scalar $results[3]->route_interesting, + 3, 'result 3: route_interesting: 3 elements' ); +is( + ( $results[3]->route_interesting )[0]->loc->name, + 'Berlin Alexanderplatz (S)', + 'result 3: route_interesting 0: name' +); +is( + ( $results[3]->route_interesting )[1]->loc->name, + 'Berlin Hackescher Markt', + 'result 3: route_interesting 1: name' +); +is( + ( $results[3]->route_interesting )[2]->loc->name, + 'Berlin Hbf (S-Bahn)', + 'result 3: route_interesting 2: name' +); + +is( scalar $results[3]->route, 16, 'result 3: route: 16 elements' ); +is( + ( $results[3]->route )[0]->loc->name, + 'Berlin Alexanderplatz (S)', + 'result 3: route 0: name' +); +is( + ( $results[3]->route )[1]->loc->name, + 'Berlin Hackescher Markt', + 'result 3: route 1: name' +); +is( + ( $results[3]->route )[2]->loc->name, + 'Berlin Friedrichstraße (S)', + 'result 3: route 2: name' +); +is( + ( $results[3]->route )[3]->loc->name, + 'Berlin Hbf (S-Bahn)', + 'result 3: route 3: name' +); +is( + ( $results[3]->route )[4]->loc->name, + 'Berlin Bellevue', + 'result 3: route 4: name' +); +is( ( $results[3]->route )[5]->loc->name, + 'Berlin-Tiergarten', 'result 3: route 5: name' ); +is( + ( $results[3]->route )[6]->loc->name, + 'Berlin Zoologischer Garten (S)', + 'result 3: route 6: name' +); +is( + ( $results[3]->route )[7]->loc->name, + 'Berlin Savignyplatz', + 'result 3: route 7: name' +); +is( + ( $results[3]->route )[8]->loc->name, + 'Berlin Charlottenburg (S)', + 'result 3: route 8: name' +); +is( + ( $results[3]->route )[9]->loc->name, + 'Berlin Westkreuz', + 'result 3: route 9: name' +); +is( + ( $results[3]->route )[10]->loc->name, + 'Berlin Messe Süd (Eichkamp)', + 'result 3: route 10: name' +); +is( + ( $results[3]->route )[11]->loc->name, + 'Berlin Heerstraße', + 'result 3: route 11: name' +); +is( + ( $results[3]->route )[12]->loc->name, + 'Berlin Olympiastadion', + 'result 3: route 12: name' +); +is( ( $results[3]->route )[13]->loc->name, + 'Berlin-Pichelsberg', 'result 3: route 17: name' ); +is( ( $results[3]->route )[14]->loc->name, + 'Berlin-Stresow', 'result 3: route 14: name' ); +is( + ( $results[3]->route )[15]->loc->name, + 'Berlin-Spandau (S)', + 'result 3: route 15: name' +); + +is( + $results[3]->sched_datetime->strftime('%Y%m%d %H%M%S'), + '20221002 170100', + 'result 3: sched_datetime' +); diff --git a/t/21-db-journeymatch.t b/t/21-db-journeymatch.t new file mode 100755 index 0000000..3e1d31e --- /dev/null +++ b/t/21-db-journeymatch.t @@ -0,0 +1,84 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use 5.020; + +use utf8; + +use File::Slurp qw(read_file); +use JSON; +use Test::More tests => 39; + +use Travel::Status::DE::HAFAS; + +my $json = JSON->new->utf8->decode( read_file('t/in/DB.ICE23.json') ); + +my $status = Travel::Status::DE::HAFAS->new( + service => 'DB', + journeyMatch => 'ICE 23', + json => $json +); + +is( $status->errcode, undef, 'no error code' ); +is( $status->errstr, undef, 'no error string' ); + +is( + $status->get_active_service->{name}, + 'Deutsche Bahn', + 'active service name' +); + +is( scalar $status->results, 1, 'number of results' ); + +my ($result) = $status->results; + +isa_ok( $result, 'Travel::Status::DE::HAFAS::Journey' ); + +is( $result->name, 'ICE 23', 'name' ); +is( $result->type, 'ICE', 'type' ); +is( $result->type_long, 'Intercity-Express', 'type_long', ); +is( $result->class, 1, 'class' ); +is( $result->line, 'ICE 23', 'line' ); +is( $result->line_no, 91, 'line_no' ); +is( $result->id, '1|196351|0|81|17122023', 'id' ); +is( $result->operator, 'DB Fernverkehr AG', 'operator' ); + +is( scalar $result->route, 2, 'route == 2' ); +is( ( $result->route )[0]->loc->name, 'Dortmund Hbf', 'route[0] name' ); +is( ( $result->route )[0]->arr, undef, 'route[0] arr' ); +is( + ( $result->route )[0]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 043400', + 'route[0] dep' +); +is( ( $result->route )[1]->loc->name, 'Passau Hbf', 'route[1]' ); +is( + ( $result->route )[1]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 122500', + 'route[1] arr' +); +is( ( $result->route )[1]->dep, undef, 'route[1] dep' ); + +is( scalar $result->route_interesting, 1, 'route_interesting == 1' ); +is( ( $result->route_interesting )[0]->loc->name, + 'Dortmund Hbf', 'route_interesting[0]' ); + +# there is no station, so corresponding accessors must be undef +is( $result->rt_datetime, undef, 'rt_datetime' ); +is( $result->sched_datetime, undef, 'sched_datetime' ); +is( $result->datetime, undef, 'sched_datetime' ); +is( $result->delay, undef, 'delay' ); +is( $result->is_cancelled, undef, 'is_cancelled' ); +is( $result->is_partially_cancelled, undef, 'is_partially_cancelled' ); +is( $result->rt_platform, undef, 'rt_platform' ); +is( $result->sched_platform, undef, 'sched_platform' ); +is( $result->platform, undef, 'platform' ); +is( $result->is_changed_platform, 0, 'is_changed_platform' ); +is( $result->load, undef, 'load' ); +is( $result->station, undef, 'station' ); +is( $result->station_eva, undef, 'station_eva' ); +is( $result->origin, undef, 'origin' ); +is( $result->destination, undef, 'destination' ); +is( $result->direction, undef, 'direction' ); + +is( scalar $result->messages, 0, 'messages' ); diff --git a/t/22-db-journey.t b/t/22-db-journey.t new file mode 100755 index 0000000..a7553a9 --- /dev/null +++ b/t/22-db-journey.t @@ -0,0 +1,336 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use 5.020; + +use utf8; + +use File::Slurp qw(read_file); +use JSON; +use Test::More tests => 144; + +use Travel::Status::DE::HAFAS; + +my $json = JSON->new->utf8->decode( read_file('t/in/DB.ICE23.journey.json') ); + +my $status = Travel::Status::DE::HAFAS->new( + service => 'DB', + journey => { id => '1|196351|0|81|17122023' }, + json => $json +); + +is( $status->errcode, undef, 'no error code' ); +is( $status->errstr, undef, 'no error string' ); + +is( + $status->get_active_service->{name}, + 'Deutsche Bahn', + 'active service name' +); + +my $result = $status->result; + +isa_ok( $result, 'Travel::Status::DE::HAFAS::Journey' ); + +is( $result->name, 'ICE 23', 'name' ); +is( $result->type, 'ICE', 'type' ); +is( $result->type_long, 'Intercity-Express', 'type_long', ); +is( $result->class, 1, 'class' ); +is( $result->line, 'ICE 23', 'line' ); +is( $result->line_no, 91, 'line_no' ); +is( $result->id, '1|196351|0|81|17122023', 'id' ); +is( $result->operator, 'DB Fernverkehr AG', 'operator' ); +is( $result->direction, 'Wien Hbf', 'direction' ); + +is( scalar $result->route, 21, 'route == 21' ); + +is( ( $result->route )[0]->loc->name, 'Dortmund Hbf', 'route[0] name' ); +is( ( $result->route )[0]->direction, 'Wien Hbf', 'route[0] direction' ); +is( ( $result->route )[0]->arr, undef, 'route[0] arr' ); +is( ( $result->route )[0]->rt_arr, undef, 'route[0] rt_arr' ); +is( + ( $result->route )[0]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 043400', + 'route[0] dep' +); +is( ( $result->route )[0]->rt_dep, undef, 'route[0] rt_dep' ); +is( ( $result->route )[0]->arr_delay, undef, 'route[0] arr_delay' ); +is( ( $result->route )[0]->dep_delay, undef, 'route[0] dep_delay' ); +is( ( $result->route )[0]->delay, undef, 'route[0] delay' ); +is( ( $result->route )[0]->load->{FIRST}, 1, 'route[0] load 1st' ); +is( ( $result->route )[0]->load->{SECOND}, 1, 'route[0] load 2nd' ); + +is( ( $result->route )[1]->loc->name, 'Bochum Hbf', 'route[1] name' ); +is( ( $result->route )[1]->direction, undef, 'route[1] direction' ); +is( + ( $result->route )[1]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 044700', + 'route[1] arr' +); +is( ( $result->route )[1]->rt_arr, undef, 'route[1] rt_arr' ); +is( + ( $result->route )[1]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 044800', + 'route[1] dep' +); +is( ( $result->route )[1]->rt_dep, undef, 'route[1] rt_dep' ); +is( ( $result->route )[1]->arr_delay, undef, 'route[1] arr_delay' ); +is( ( $result->route )[1]->dep_delay, undef, 'route[1] dep_delay' ); +is( ( $result->route )[1]->delay, undef, 'route[1] delay' ); +is( ( $result->route )[1]->load->{FIRST}, 1, 'route[1] load 1st' ); +is( ( $result->route )[1]->load->{SECOND}, 1, 'route[1] load 2nd' ); + +is( ( $result->route )[2]->loc->name, 'Essen Hbf', 'route[2] name' ); +is( + ( $result->route )[2]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 045900', + 'route[2] arr' +); +is( ( $result->route )[2]->rt_arr, undef, 'route[2] rt_arr' ); +is( + ( $result->route )[2]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 050100', + 'route[2] dep' +); +is( ( $result->route )[2]->rt_dep, undef, 'route[2] rt_dep' ); +is( ( $result->route )[2]->arr_delay, undef, 'route[2] arr_delay' ); +is( ( $result->route )[2]->dep_delay, undef, 'route[2] dep_delay' ); +is( ( $result->route )[2]->delay, undef, 'route[2] delay' ); +is( ( $result->route )[2]->load->{FIRST}, 1, 'route[2] load 1st' ); +is( ( $result->route )[2]->load->{SECOND}, 1, 'route[2] load 2nd' ); + +is( ( $result->route )[8]->loc->name, 'Mainz Hbf', 'route[8] name' ); +is( + ( $result->route )[8]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 073800', + 'route[8] arr' +); +is( ( $result->route )[8]->rt_arr, undef, 'route[8] rt_arr' ); +is( + ( $result->route )[8]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 074000', + 'route[8] dep' +); +is( ( $result->route )[8]->rt_dep, undef, 'route[8] rt_dep' ); +is( ( $result->route )[8]->arr_delay, undef, 'route[8] arr_delay' ); +is( ( $result->route )[8]->dep_delay, undef, 'route[8] dep_delay' ); +is( ( $result->route )[8]->delay, undef, 'route[8] delay' ); +is( ( $result->route )[8]->load->{FIRST}, 1, 'route[8] load 1st' ); +is( ( $result->route )[8]->load->{SECOND}, 2, 'route[8] load 2nd' ); + +is( + ( $result->route )[9]->loc->name, + 'Frankfurt(M) Flughafen Fernbf', + 'route[9] name' +); +is( + ( $result->route )[9]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 075900', + 'route[9] arr' +); +is( ( $result->route )[9]->rt_arr, undef, 'route[9] rt_arr' ); +is( + ( $result->route )[9]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 080200', + 'route[9] dep' +); +is( ( $result->route )[9]->rt_dep, undef, 'route[9] rt_dep' ); +is( ( $result->route )[9]->arr_delay, undef, 'route[9] arr_delay' ); +is( ( $result->route )[9]->dep_delay, undef, 'route[9] dep_delay' ); +is( ( $result->route )[9]->delay, undef, 'route[9] delay' ); +is( ( $result->route )[9]->load->{FIRST}, undef, 'route[9] load 1st' ); +is( ( $result->route )[9]->load->{SECOND}, undef, 'route[9] load 2nd' ); + +is( ( $result->route )[10]->loc->name, 'Frankfurt(Main)Hbf', 'route[10] name' ); +is( + ( $result->route )[10]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 081400', + 'route[10] arr' +); +is( ( $result->route )[10]->rt_arr, undef, 'route[10] rt_arr' ); +is( + ( $result->route )[10]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 082100', + 'route[10] dep' +); +is( ( $result->route )[10]->rt_dep, undef, 'route[10] rt_dep' ); +is( ( $result->route )[10]->arr_delay, undef, 'route[10] arr_delay' ); +is( ( $result->route )[10]->dep_delay, undef, 'route[10] dep_delay' ); +is( ( $result->route )[10]->delay, undef, 'route[10] delay' ); +is( ( $result->route )[10]->load->{FIRST}, 1, 'route[10] load 1st' ); +is( ( $result->route )[10]->load->{SECOND}, 2, 'route[10] load 2nd' ); + +is( ( $result->route )[12]->loc->name, 'Würzburg Hbf', 'route[12] name' ); +is( + ( $result->route )[12]->sched_arr->strftime('%Y%m%d %H%M%S'), + '20231217 093200', + 'route[12] sched_arr' +); +is( + ( $result->route )[12]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 093300', + 'route[12] arr' +); +is( + ( $result->route )[12]->rt_arr->strftime('%Y%m%d %H%M%S'), + '20231217 093300', + 'route[12] arr' +); +is( + ( $result->route )[12]->sched_dep->strftime('%Y%m%d %H%M%S'), + '20231217 093400', + 'route[12] sched_dep' +); +is( + ( $result->route )[12]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 093600', + 'route[12] dep' +); +is( + ( $result->route )[12]->rt_dep->strftime('%Y%m%d %H%M%S'), + '20231217 093600', + 'route[12] dep' +); +is( ( $result->route )[12]->arr_delay, 1, 'route[12] arr_delay' ); +is( ( $result->route )[12]->dep_delay, 2, 'route[12] dep_delay' ); +is( ( $result->route )[12]->delay, 2, 'route[12] delay' ); +is( ( $result->route )[12]->load->{FIRST}, 2, 'route[12] load 1st' ); +is( ( $result->route )[12]->load->{SECOND}, 2, 'route[12] load 2nd' ); + +is( ( $result->route )[13]->loc->name, 'Nürnberg Hbf', 'route[13] name' ); +is( + ( $result->route )[13]->sched_arr->strftime('%Y%m%d %H%M%S'), + '20231217 102700', + 'route[13] sched_arr' +); +is( + ( $result->route )[13]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 102900', + 'route[13] arr' +); +is( + ( $result->route )[13]->rt_arr->strftime('%Y%m%d %H%M%S'), + '20231217 102900', + 'route[13] arr' +); +is( + ( $result->route )[13]->sched_dep->strftime('%Y%m%d %H%M%S'), + '20231217 103100', + 'route[13] sched_dep' +); +is( + ( $result->route )[13]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 103300', + 'route[13] dep' +); +is( + ( $result->route )[13]->rt_dep->strftime('%Y%m%d %H%M%S'), + '20231217 103300', + 'route[13] dep' +); +is( ( $result->route )[13]->arr_delay, 2, 'route[13] arr_delay' ); +is( ( $result->route )[13]->dep_delay, 2, 'route[13] dep_delay' ); +is( ( $result->route )[13]->delay, 2, 'route[13] delay' ); +is( ( $result->route )[13]->load->{FIRST}, 3, 'route[13] load 1st' ); +is( ( $result->route )[13]->load->{SECOND}, 2, 'route[13] load 2nd' ); + +is( ( $result->route )[15]->loc->name, 'Plattling', 'route[15] name' ); +is( + ( $result->route )[15]->sched_arr->strftime('%Y%m%d %H%M%S'), + '20231217 115700', + 'route[15] sched_arr' +); +is( + ( $result->route )[15]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 115700', + 'route[15] arr' +); +is( + ( $result->route )[15]->rt_arr->strftime('%Y%m%d %H%M%S'), + '20231217 115700', + 'route[15] arr' +); +is( + ( $result->route )[15]->sched_dep->strftime('%Y%m%d %H%M%S'), + '20231217 115900', + 'route[15] sched_dep' +); +is( + ( $result->route )[15]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 115900', + 'route[15] dep' +); +is( + ( $result->route )[15]->rt_dep->strftime('%Y%m%d %H%M%S'), + '20231217 115900', + 'route[15] dep' +); +is( ( $result->route )[15]->arr_delay, 0, 'route[15] arr_delay' ); +is( ( $result->route )[15]->dep_delay, 0, 'route[15] dep_delay' ); +is( ( $result->route )[15]->delay, 0, 'route[15] delay' ); +is( ( $result->route )[15]->load->{FIRST}, 2, 'route[15] load 1st' ); +is( ( $result->route )[15]->load->{SECOND}, 2, 'route[15] load 2nd' ); + +is( ( $result->route )[16]->loc->name, 'Passau Hbf', 'route[16] name' ); +is( + ( $result->route )[16]->sched_arr->strftime('%Y%m%d %H%M%S'), + '20231217 122500', + 'route[16] sched_arr' +); +is( + ( $result->route )[16]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 122700', + 'route[16] arr' +); +is( + ( $result->route )[16]->rt_arr->strftime('%Y%m%d %H%M%S'), + '20231217 122700', + 'route[16] arr' +); +is( + ( $result->route )[16]->sched_dep->strftime('%Y%m%d %H%M%S'), + '20231217 122900', + 'route[16] sched_dep' +); +is( + ( $result->route )[16]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 122900', + 'route[16] dep' +); +is( + ( $result->route )[16]->rt_dep->strftime('%Y%m%d %H%M%S'), + '20231217 122900', + 'route[16] dep' +); +is( ( $result->route )[16]->arr_delay, 2, 'route[16] arr_delay' ); +is( ( $result->route )[16]->dep_delay, 0, 'route[16] dep_delay' ); +is( ( $result->route )[16]->delay, 0, 'route[16] delay' ); +is( ( $result->route )[16]->load->{FIRST}, undef, 'route[16] load 1st' ); +is( ( $result->route )[16]->load->{SECOND}, undef, 'route[16] load 2nd' ); + +is( scalar $result->route_interesting, 3, 'route_interesting == 3' ); +is( ( $result->route_interesting )[0]->loc->name, + 'Dortmund Hbf', 'route_interesting[0]' ); +is( ( $result->route_interesting )[1]->loc->name, + 'Bochum Hbf', 'route_interesting[1]' ); +is( ( $result->route_interesting )[2]->loc->name, + 'Essen Hbf', 'route_interesting[2]' ); + +# there is no station, so corresponding accessors must be undef +is( $result->rt_datetime, undef, 'rt_datetime' ); +is( $result->sched_datetime, undef, 'sched_datetime' ); +is( $result->datetime, undef, 'sched_datetime' ); +is( $result->delay, undef, 'delay' ); +is( $result->is_cancelled, undef, 'is_cancelled' ); +is( $result->is_partially_cancelled, undef, 'is_partially_cancelled' ); +is( $result->rt_platform, undef, 'rt_platform' ); +is( $result->sched_platform, undef, 'sched_platform' ); +is( $result->platform, undef, 'platform' ); +is( $result->is_changed_platform, 0, 'is_changed_platform' ); +is( $result->load, undef, 'load' ); +is( $result->station, undef, 'station' ); +is( $result->station_eva, undef, 'station_eva' ); +is( $result->origin, undef, 'origin' ); +is( $result->destination, undef, 'destination' ); + +is( scalar $result->messages, 12, 'messages' ); diff --git a/t/30-db-journey-platformchange.t b/t/30-db-journey-platformchange.t new file mode 100755 index 0000000..ea4f177 --- /dev/null +++ b/t/30-db-journey-platformchange.t @@ -0,0 +1,75 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use 5.020; + +use utf8; + +use File::Slurp qw(read_file); +use JSON; +use Test::More tests => 30; + +use Travel::Status::DE::HAFAS; + +my $json = JSON->new->utf8->decode( read_file('t/in/DB.EC392.journey.json') ); + +my $status = Travel::Status::DE::HAFAS->new( + service => 'DB', + journey => { id => '1|197782|0|81|17122023' }, + json => $json +); + +is( $status->errcode, undef, 'no error code' ); +is( $status->errstr, undef, 'no error string' ); + +is( + $status->get_active_service->{name}, + 'Deutsche Bahn', + 'active service name' +); + +my $result = $status->result; + +isa_ok( $result, 'Travel::Status::DE::HAFAS::Journey' ); + +is( $result->name, 'EC 392', 'name' ); +is( $result->type, 'EC', 'type' ); +is( $result->type_long, 'Eurocity', 'type_long', ); +is( $result->class, 2, 'class' ); +is( $result->line, 'EC 392', 'line' ); +is( $result->line_no, 75, 'line_no' ); +is( $result->id, '1|197782|0|81|17122023', 'id' ); +is( $result->operator, 'DB Fernverkehr AG', 'operator' ); +is( $result->direction, 'Koebenhavn H', 'direction' ); + +is( scalar $result->route, 7, 'route == 7' ); + +is( ( $result->route )[0]->loc->name, 'Hamburg Hbf', 'route[0] name' ); +is( ( $result->route )[0]->direction, 'Koebenhavn H', 'route[0] direction' ); +is( ( $result->route )[0]->arr, undef, 'route[0] arr' ); +is( ( $result->route )[0]->rt_arr, undef, 'route[0] rt_arr' ); +is( + ( $result->route )[0]->sched_dep->strftime('%Y%m%d %H%M%S'), + '20231217 145300', + 'route[0] dep' +); +is( + ( $result->route )[0]->rt_dep->strftime('%Y%m%d %H%M%S'), + '20231217 150300', + 'route[0] dep' +); +is( + ( $result->route )[0]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 150300', + 'route[0] dep' +); +is( ( $result->route )[0]->arr_delay, undef, 'route[0] arr_delay' ); +is( ( $result->route )[0]->dep_delay, 10, 'route[0] dep_delay' ); +is( ( $result->route )[0]->delay, 10, 'route[0] delay' ); +is( ( $result->route )[0]->load->{FIRST}, 3, 'route[0] load 1st' ); +is( ( $result->route )[0]->load->{SECOND}, 3, 'route[0] load 2nd' ); +is( ( $result->route )[0]->sched_platform, '12C-F', 'route[0] sched_platform' ); +is( ( $result->route )[0]->rt_platform, '12A-B', 'route[0] rt_platform' ); +is( ( $result->route )[0]->platform, '12A-B', 'route[0] rt_platform' ); +ok( ( $result->route )[0]->is_changed_platform, + 'route[0] is_changed_platform' ); diff --git a/t/31-db-journey-daychange.t b/t/31-db-journey-daychange.t new file mode 100755 index 0000000..bb66cdb --- /dev/null +++ b/t/31-db-journey-daychange.t @@ -0,0 +1,88 @@ +#!/usr/bin/env perl +use strict; +use warnings; +use 5.020; + +use utf8; + +use File::Slurp qw(read_file); +use JSON; +use Test::More tests => 36; + +use Travel::Status::DE::HAFAS; + +my $json = JSON->new->utf8->decode( read_file('t/in/DB.ICE615.journey.json') ); + +my $status = Travel::Status::DE::HAFAS->new( + service => 'DB', + journey => { id => '1|160139|0|81|17122023' }, + json => $json +); + +is( $status->errcode, undef, 'no error code' ); +is( $status->errstr, undef, 'no error string' ); + +is( + $status->get_active_service->{name}, + 'Deutsche Bahn', + 'active service name' +); + +my $result = $status->result; + +isa_ok( $result, 'Travel::Status::DE::HAFAS::Journey' ); + +is( $result->name, 'ICE 615', 'name' ); +is( $result->type, 'ICE', 'type' ); +is( $result->type_long, 'Intercity-Express', 'type_long', ); +is( $result->class, 1, 'class' ); +is( $result->line, 'ICE 615', 'line' ); +is( $result->line_no, 42, 'line_no' ); +is( $result->id, '1|160139|0|81|17122023', 'id' ); +is( $result->operator, 'DB Fernverkehr AG', 'operator' ); +is( $result->direction, 'München Hbf', 'direction' ); + +is( scalar $result->route, 19, 'route == 19' ); + +is( ( $result->route )[0]->loc->name, 'Hamburg-Altona', 'route[0] name' ); +is( ( $result->route )[0]->direction, 'München Hbf', 'route[0] direction' ); + +is( ( $result->route )[4]->loc->name, 'Bremen Hbf', 'route[4] name' ); +is( + ( $result->route )[4]->direction, + 'Frankfurt(M) Flughafen Fernbf', + 'route[4] direction' +); + +is( ( $result->route )[5]->loc->name, 'Osnabrück Hbf', 'route[5] name' ); +is( ( $result->route )[5]->direction, 'München Hbf', 'route[5] direction' ); + +is( ( $result->route )[16]->loc->name, 'Augsburg Hbf', 'route[16] name' ); +is( + ( $result->route )[16]->arr->strftime('%Y%m%d %H%M%S'), + '20231217 235300', + 'route[16] arr' +); +is( ( $result->route )[16]->rt_arr, undef, 'route[16] rt_arr' ); +is( + ( $result->route )[16]->dep->strftime('%Y%m%d %H%M%S'), + '20231217 235500', + 'route[16] dep' +); +is( ( $result->route )[16]->rt_dep, undef, 'route[16] rt_dep' ); +is( ( $result->route )[16]->arr_delay, undef, 'route[16] arr_delay' ); +is( ( $result->route )[16]->dep_delay, undef, 'route[16] dep_delay' ); +is( ( $result->route )[16]->delay, undef, 'route[16] delay' ); + +is( ( $result->route )[17]->loc->name, 'München-Pasing', 'route[17] name' ); +is( + ( $result->route )[17]->arr->strftime('%Y%m%d %H%M%S'), + '20231218 001700', + 'route[17] arr' +); +is( ( $result->route )[17]->rt_arr, undef, 'route[17] rt_arr' ); +is( ( $result->route )[17]->dep, undef, 'route[17] dep' ); +is( ( $result->route )[17]->rt_dep, undef, 'route[17] rt_dep' ); +is( ( $result->route )[17]->arr_delay, undef, 'route[17] arr_delay' ); +is( ( $result->route )[17]->dep_delay, undef, 'route[17] dep_delay' ); +is( ( $result->route )[17]->delay, undef, 'route[17] delay' ); diff --git a/t/in/DB.Berlin Jannowitzbrücke.json b/t/in/DB.Berlin Jannowitzbrücke.json new file mode 100644 index 0000000..e8ab4bb --- /dev/null +++ b/t/in/DB.Berlin Jannowitzbrücke.json @@ -0,0 +1 @@ +{"ver":"1.15","ext":"DB.R21.12.a","lang":"deu","id":"8g46emqmmw2swm4s","cInfo":{"code":"OK"},"svcResL":[{"meth":"StationBoard","err":"OK","res":{"common":{"locL":[{"lid":"A=1@O=Jannowitzbrücke (S+U), Berlin@X=13418126@Y=52515503@U=80@L=732534@","type":"S","name":"Jannowitzbrücke (S+U), Berlin","icoX":0,"extId":"732534","state":"F","crd":{"x":13418126,"y":52515503,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":184,"pRefL":[1,2,3,4,5,6],"mMastLocX":1},{"lid":"A=1@O=Berlin Jannowitzbrücke@X=13419349@Y=52514272@U=80@L=8089019@","type":"S","name":"Berlin Jannowitzbrücke","icoX":0,"extId":"8089019","state":"F","crd":{"x":13419681,"y":52514227,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":184,"pRefL":[7,8,9,10,11,12,1,2,3,4,5,6]},{"lid":"A=1@O=Philharmonie Süd, Berlin@X=13370816@Y=52509166@U=80@L=744896@","type":"S","name":"Philharmonie Süd, Berlin","icoX":0,"extId":"744896","state":"F","crd":{"x":13370816,"y":52509166,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=Alexanderstr., Berlin@X=13418072@Y=52517121@U=80@L=732595@","type":"S","name":"Alexanderstr., Berlin","icoX":0,"extId":"732595","state":"F","crd":{"x":13418072,"y":52517121,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=Alexanderplatz (S+U)/Grunerstr., Berlin@X=13414926@Y=52520825@U=80@L=732536@","type":"S","name":"Alexanderplatz (S+U)/Grunerstr., Berlin","icoX":0,"extId":"732536","state":"F","crd":{"x":13414926,"y":52520825,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":440,"mMastLocX":5},{"lid":"A=1@O=Berlin Alexanderplatz@X=13410962@Y=52521481@U=80@L=8011155@","type":"S","name":"Berlin Alexanderplatz","icoX":0,"extId":"8011155","state":"F","crd":{"x":13411088,"y":52521526,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":440},{"lid":"A=1@O=Rotes Rathaus (U), Berlin@X=13409209@Y=52517032@U=80@L=732572@","type":"S","name":"Rotes Rathaus (U), Berlin","icoX":0,"extId":"732572","state":"F","crd":{"x":13409209,"y":52517032,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Museumsinsel (U), Berlin@X=13400283@Y=52518083@U=80@L=732613@","type":"S","name":"Museumsinsel (U), Berlin","icoX":0,"extId":"732613","state":"F","crd":{"x":13400283,"y":52518083,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Staatsoper, Berlin@X=13393837@Y=52517319@U=80@L=732591@","type":"S","name":"Staatsoper, Berlin","icoX":0,"extId":"732591","state":"F","crd":{"x":13393837,"y":52517319,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=Unter den Linden (U), Berlin@X=13388830@Y=52517041@U=80@L=732590@","type":"S","name":"Unter den Linden (U), Berlin","icoX":0,"extId":"732590","state":"F","crd":{"x":13388830,"y":52517041,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Behrenstr./Wilhelmstr., Berlin@X=13381558@Y=52514964@U=80@L=746255@","type":"S","name":"Behrenstr./Wilhelmstr., Berlin","icoX":0,"extId":"746255","state":"F","crd":{"x":13381558,"y":52514964,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":40},{"lid":"A=1@O=Mohrenstr. (U), Berlin@X=13384471@Y=52511638@U=80@L=732540@","type":"S","name":"Mohrenstr. (U), Berlin","icoX":0,"extId":"732540","state":"F","crd":{"x":13384471,"y":52511638,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Leipziger Str./Wilhelmstr., Berlin@X=13383536@Y=52509957@U=80@L=732611@","type":"S","name":"Leipziger Str./Wilhelmstr., Berlin","icoX":0,"extId":"732611","state":"F","crd":{"x":13383536,"y":52509957,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=Potsdamer Platz [Bus Leipziger Str.] (S+U), Berlin@X=13378286@Y=52509615@U=80@L=728614@","type":"S","name":"Potsdamer Platz [Bus Leipziger Str.] (S+U), Berlin","icoX":0,"extId":"728614","state":"F","crd":{"x":13378286,"y":52509615,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":184,"mMastLocX":14},{"lid":"A=1@O=Berlin Potsdamer Platz@X=13375985@Y=52509382@U=80@L=8011118@","type":"S","name":"Berlin Potsdamer Platz","icoX":0,"extId":"8011118","state":"F","crd":{"x":13375904,"y":52509436,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":184},{"lid":"A=1@O=Varian-Fry-Str./Potsdamer Platz, Berlin@X=13374376@Y=52509346@U=80@L=746250@","type":"S","name":"Varian-Fry-Str./Potsdamer Platz, Berlin","icoX":0,"extId":"746250","state":"F","crd":{"x":13374376,"y":52509346,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=Paracelsus-Bad (U), Berlin@X=13348145@Y=52574284@U=80@L=732266@","type":"S","name":"Paracelsus-Bad (U), Berlin","icoX":0,"extId":"732266","state":"F","crd":{"x":13348145,"y":52574284,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":168},{"lid":"A=1@O=Alexanderplatz [U8] (S+U), Berlin@X=13412661@Y=52521023@U=80@L=732533@","type":"S","name":"Alexanderplatz [U8] (S+U), Berlin","icoX":2,"extId":"732533","state":"F","crd":{"x":13412661,"y":52521023,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":440,"mMastLocX":5},{"lid":"A=1@O=Weinmeisterstr. (U), Berlin@X=13405299@Y=52525356@U=80@L=732576@","type":"S","name":"Weinmeisterstr. (U), Berlin","icoX":0,"extId":"732576","state":"F","crd":{"x":13405299,"y":52525356,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":416},{"lid":"A=1@O=Rosenthaler Platz (U), Berlin@X=13401406@Y=52529787@U=80@L=732552@","type":"S","name":"Rosenthaler Platz (U), Berlin","icoX":0,"extId":"732552","state":"F","crd":{"x":13401406,"y":52529787,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":416},{"lid":"A=1@O=Bernauer Str. (U), Berlin@X=13396148@Y=52538039@U=80@L=730803@","type":"S","name":"Bernauer Str. (U), Berlin","icoX":0,"extId":"730803","state":"F","crd":{"x":13396148,"y":52538039,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":416},{"lid":"A=1@O=Voltastr. (U), Berlin@X=13393397@Y=52541590@U=80@L=730797@","type":"S","name":"Voltastr. (U), Berlin","icoX":0,"extId":"730797","state":"F","crd":{"x":13393397,"y":52541590,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Gesundbrunnen Bahnhof (S+U), Berlin@X=13388264@Y=52548970@U=80@L=730796@","type":"S","name":"Gesundbrunnen Bahnhof (S+U), Berlin","icoX":0,"extId":"730796","state":"F","crd":{"x":13388264,"y":52548970,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":191,"mMastLocX":23},{"lid":"A=1@O=Berlin Gesundbrunnen@X=13388516@Y=52548961@U=80@L=8011102@","type":"S","name":"Berlin Gesundbrunnen","icoX":6,"extId":"8011102","state":"F","crd":{"x":13391060,"y":52548656,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":191},{"lid":"A=1@O=Osloer Str. (U), Berlin@X=13373126@Y=52556728@U=80@L=730846@","type":"S","name":"Osloer Str. (U), Berlin","icoX":0,"extId":"730846","state":"F","crd":{"x":13373126,"y":52556728,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":416},{"lid":"A=1@O=Franz-Neumann-Platz (Am Schäfersee) (U), Berlin@X=13364029@Y=52564360@U=80@L=732278@","type":"S","name":"Franz-Neumann-Platz (Am Schäfersee) (U), Berlin","icoX":0,"extId":"732278","state":"F","crd":{"x":13364029,"y":52564360,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Residenzstr. (U), Berlin@X=13360667@Y=52570733@U=80@L=732279@","type":"S","name":"Residenzstr. (U), Berlin","icoX":0,"extId":"732279","state":"F","crd":{"x":13360667,"y":52570733,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Hermannstr. (S+U), Berlin@X=13431349@Y=52467465@U=80@L=732126@","type":"S","name":"Hermannstr. (S+U), Berlin","icoX":0,"extId":"732126","state":"F","crd":{"x":13431349,"y":52467465,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":184,"mMastLocX":28},{"lid":"A=1@O=Berlin Hermannstraße@X=13431313@Y=52467510@U=80@L=8089105@","type":"S","name":"Berlin Hermannstraße","icoX":0,"extId":"8089105","state":"F","crd":{"x":13430945,"y":52467645,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":184},{"lid":"A=1@O=Heinrich-Heine-Str. (U), Berlin@X=13416185@Y=52510622@U=80@L=732538@","type":"S","name":"Heinrich-Heine-Str. (U), Berlin","icoX":0,"extId":"732538","state":"F","crd":{"x":13416185,"y":52510622,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Moritzplatz (U), Berlin@X=13410620@Y=52503647@U=80@L=730889@","type":"S","name":"Moritzplatz (U), Berlin","icoX":0,"extId":"730889","state":"F","crd":{"x":13410620,"y":52503647,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Kottbusser Tor (U), Berlin@X=13418198@Y=52499071@U=80@L=730890@","type":"S","name":"Kottbusser Tor (U), Berlin","icoX":0,"extId":"730890","state":"F","crd":{"x":13418198,"y":52499071,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Schönleinstr. (U), Berlin@X=13421848@Y=52493381@U=80@L=730927@","type":"S","name":"Schönleinstr. (U), Berlin","icoX":0,"extId":"730927","state":"F","crd":{"x":13421848,"y":52493381,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Hermannplatz (U), Berlin@X=13424428@Y=52486576@U=80@L=732102@","type":"S","name":"Hermannplatz (U), Berlin","icoX":0,"extId":"732102","state":"F","crd":{"x":13424428,"y":52486576,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Boddinstr. (U), Berlin@X=13425408@Y=52479798@U=80@L=732121@","type":"S","name":"Boddinstr. (U), Berlin","icoX":0,"extId":"732121","state":"F","crd":{"x":13425408,"y":52479798,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Leinestr. (U), Berlin@X=13428131@Y=52473425@U=80@L=732120@","type":"S","name":"Leinestr. (U), Berlin","icoX":0,"extId":"732120","state":"F","crd":{"x":13428131,"y":52473425,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Berlin-Spandau (S)@X=13197450@Y=52534776@U=80@L=8089083@","type":"S","name":"Berlin-Spandau (S)","icoX":0,"extId":"8089083","state":"F","crd":{"x":13198547,"y":52534632,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":191,"entry":true,"mMastLocX":37},{"lid":"A=1@O=Berlin-Spandau@X=13196902@Y=52534650@U=80@L=8010404@","type":"S","name":"Berlin-Spandau","icoX":6,"extId":"8010404","state":"F","crd":{"x":13200947,"y":52533787,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":191},{"lid":"A=1@O=Berlin Alexanderplatz (S)@X=13411007@Y=52521643@U=80@L=8089001@","type":"S","name":"Berlin Alexanderplatz (S)","icoX":3,"extId":"8089001","state":"F","crd":{"x":13411097,"y":52521643,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":440,"entry":true,"mMastLocX":5},{"lid":"A=1@O=Berlin Hackescher Markt@X=13402368@Y=52522623@U=80@L=8089017@","type":"S","name":"Berlin Hackescher Markt","icoX":3,"extId":"8089017","state":"F","crd":{"x":13402197,"y":52522614,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":304},{"lid":"A=1@O=Berlin Friedrichstraße (S)@X=13386907@Y=52520178@U=80@L=8089066@","type":"S","name":"Berlin Friedrichstraße (S)","icoX":3,"extId":"8089066","state":"F","crd":{"x":13386322,"y":52520555,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":444,"entry":true,"mMastLocX":41},{"lid":"A=1@O=Berlin Friedrichstraße@X=13387203@Y=52520376@U=80@L=8011306@","type":"S","name":"Berlin Friedrichstraße","icoX":8,"extId":"8011306","state":"F","crd":{"x":13386925,"y":52520331,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":444},{"lid":"A=1@O=Berlin Hbf (S-Bahn)@X=13369549@Y=52525589@U=80@L=8089021@","type":"S","name":"Berlin Hbf (S-Bahn)","icoX":3,"extId":"8089021","state":"F","crd":{"x":13369279,"y":52525167,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":447,"entry":true,"mMastLocX":43},{"lid":"A=1@O=Berlin Hbf@X=13369549@Y=52525589@U=80@L=8011160@","type":"S","name":"Berlin Hbf","icoX":6,"extId":"8011160","state":"F","crd":{"x":13369629,"y":52524924,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":447},{"lid":"A=1@O=Berlin Bellevue@X=13348262@Y=52519953@U=80@L=8089005@","type":"S","name":"Berlin Bellevue","icoX":3,"extId":"8089005","state":"F","crd":{"x":13347956,"y":52519962,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":16},{"lid":"A=1@O=Berlin-Tiergarten@X=13336396@Y=52514065@U=80@L=8089091@","type":"S","name":"Berlin-Tiergarten","icoX":0,"extId":"8089091","state":"F","crd":{"x":13336414,"y":52514281,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Berlin Zoologischer Garten (S)@X=13332360@Y=52507152@U=80@L=8089046@","type":"S","name":"Berlin Zoologischer Garten (S)","icoX":3,"extId":"8089046","state":"F","crd":{"x":13332360,"y":52507152,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":191,"entry":true,"mMastLocX":47},{"lid":"A=1@O=Berlin Zoologischer Garten@X=13331992@Y=52507278@U=80@L=8010406@","type":"S","name":"Berlin Zoologischer Garten","icoX":6,"extId":"8010406","state":"F","crd":{"x":13332414,"y":52507242,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":191},{"lid":"A=1@O=Berlin Savignyplatz@X=13319272@Y=52505094@U=80@L=8089037@","type":"S","name":"Berlin Savignyplatz","icoX":3,"extId":"8089037","state":"F","crd":{"x":13319362,"y":52505193,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin Charlottenburg (S)@X=13303945@Y=52504932@U=80@L=8089165@","type":"S","name":"Berlin Charlottenburg (S)","icoX":3,"extId":"8089165","state":"F","crd":{"x":13301338,"y":52504348,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":63,"entry":true,"mMastLocX":50},{"lid":"A=1@O=Berlin-Charlottenburg@X=13302327@Y=52504213@U=80@L=8010403@","type":"S","name":"Berlin-Charlottenburg","icoX":9,"extId":"8010403","state":"F","crd":{"x":13302049,"y":52504195,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":63},{"lid":"A=1@O=Berlin Westkreuz@X=13283962@Y=52500734@U=80@L=8089047@","type":"S","name":"Berlin Westkreuz","icoX":3,"extId":"8089047","state":"F","crd":{"x":13283962,"y":52500734,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin Messe Süd (Eichkamp)@X=13270119@Y=52498748@U=80@L=8089328@","type":"S","name":"Berlin Messe Süd (Eichkamp)","icoX":3,"extId":"8089328","state":"F","crd":{"x":13269921,"y":52498756,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin Heerstraße@X=13259377@Y=52508123@U=80@L=8089329@","type":"S","name":"Berlin Heerstraße","icoX":3,"extId":"8089329","state":"F","crd":{"x":13259952,"y":52507899,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin Olympiastadion@X=13242387@Y=52511162@U=80@L=8089330@","type":"S","name":"Berlin Olympiastadion","icoX":3,"extId":"8089330","state":"F","crd":{"x":13242765,"y":52511305,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":16},{"lid":"A=1@O=Berlin-Pichelsberg@X=13227132@Y=52510389@U=80@L=8089331@","type":"S","name":"Berlin-Pichelsberg","icoX":3,"extId":"8089331","state":"F","crd":{"x":13227087,"y":52510442,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":16},{"lid":"A=1@O=Berlin-Stresow@X=13209127@Y=52531972@U=80@L=8089053@","type":"S","name":"Berlin-Stresow","icoX":3,"extId":"8089053","state":"F","crd":{"x":13209559,"y":52531954,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Erkner (S)@X=13751761@Y=52428847@U=80@L=8089181@","type":"S","name":"Erkner (S)","icoX":3,"extId":"8089181","state":"F","crd":{"x":13751761,"y":52428847,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":568,"entry":true,"mMastLocX":58},{"lid":"A=1@O=Erkner@X=13752246@Y=52427562@U=80@L=8013477@","type":"S","name":"Erkner","icoX":0,"extId":"8013477","state":"F","crd":{"x":13751518,"y":52428236,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":568},{"lid":"A=1@O=Berlin Ostbahnhof (S)@X=13435125@Y=52510721@U=80@L=8089185@","type":"S","name":"Berlin Ostbahnhof (S)","icoX":3,"extId":"8089185","state":"F","crd":{"x":13435125,"y":52510721,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":63,"entry":true,"mMastLocX":60},{"lid":"A=1@O=Berlin Ostbahnhof@X=13434684@Y=52510487@U=80@L=8010255@","type":"S","name":"Berlin Ostbahnhof","icoX":6,"extId":"8010255","state":"F","crd":{"x":13434900,"y":52510424,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":63},{"lid":"A=1@O=Berlin Warschauer Straße@X=13451647@Y=52505975@U=80@L=8089045@","type":"S","name":"Berlin Warschauer Straße","icoX":3,"extId":"8089045","state":"F","crd":{"x":13452240,"y":52505948,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":432},{"lid":"A=1@O=Berlin Ostkreuz (S)@X=13469311@Y=52502999@U=80@L=8089028@","type":"S","name":"Berlin Ostkreuz (S)","icoX":3,"extId":"8089028","state":"F","crd":{"x":13468861,"y":52503305,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":63,"entry":true,"mMastLocX":63},{"lid":"A=1@O=Berlin Ostkreuz@X=13469104@Y=52503035@U=80@L=8011162@","type":"S","name":"Berlin Ostkreuz","icoX":6,"extId":"8011162","state":"F","crd":{"x":13470497,"y":52504689,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":63},{"lid":"A=1@O=Berlin-Rummelsburg@X=13478363@Y=52501309@U=80@L=8089084@","type":"S","name":"Berlin-Rummelsburg","icoX":3,"extId":"8089084","state":"F","crd":{"x":13479145,"y":52501031,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":304},{"lid":"A=1@O=Berlin Betriebsbf Rummelsburg@X=13497789@Y=52493830@U=80@L=8089006@","type":"S","name":"Berlin Betriebsbf Rummelsburg","icoX":3,"extId":"8089006","state":"F","crd":{"x":13497573,"y":52493920,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":16},{"lid":"A=1@O=Berlin-Karlshorst (S)@X=13526446@Y=52480832@U=80@L=8089193@","type":"S","name":"Berlin-Karlshorst (S)","icoX":3,"extId":"8089193","state":"F","crd":{"x":13526446,"y":52480832,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":304,"entry":true,"mMastLocX":67},{"lid":"A=1@O=Berlin-Karlshorst@X=13527947@Y=52479798@U=80@L=8010035@","type":"S","name":"Berlin-Karlshorst","icoX":3,"extId":"8010035","state":"F","crd":{"x":13529017,"y":52479574,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":304},{"lid":"A=1@O=Berlin-Wuhlheide@X=13553198@Y=52469029@U=80@L=8089097@","type":"S","name":"Berlin-Wuhlheide","icoX":3,"extId":"8089097","state":"F","crd":{"x":13554259,"y":52468553,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin-Köpenick@X=13580939@Y=52458611@U=80@L=8089069@","type":"S","name":"Berlin-Köpenick","icoX":0,"extId":"8089069","state":"F","crd":{"x":13579932,"y":52458692,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312},{"lid":"A=1@O=Berlin-Hirschgarten@X=13602135@Y=52457972@U=80@L=8089065@","type":"S","name":"Berlin-Hirschgarten","icoX":0,"extId":"8089065","state":"F","crd":{"x":13603151,"y":52457865,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":24},{"lid":"A=1@O=Berlin-Friedrichshagen@X=13624599@Y=52457244@U=80@L=8089060@","type":"S","name":"Berlin-Friedrichshagen","icoX":0,"extId":"8089060","state":"F","crd":{"x":13625211,"y":52457271,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312},{"lid":"A=1@O=Berlin-Rahnsdorf@X=13690401@Y=52451536@U=80@L=8089082@","type":"S","name":"Berlin-Rahnsdorf","icoX":0,"extId":"8089082","state":"F","crd":{"x":13690760,"y":52451500,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312},{"lid":"A=1@O=Berlin-Wilhelmshagen@X=13722141@Y=52438709@U=80@L=8089094@","type":"S","name":"Berlin-Wilhelmshagen","icoX":0,"extId":"8089094","state":"F","crd":{"x":13722402,"y":52438358,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Wittenau [U8] (S+U), Berlin@X=13335515@Y=52595660@U=80@L=728428@","type":"S","name":"Wittenau [U8] (S+U), Berlin","icoX":2,"extId":"728428","state":"F","crd":{"x":13335515,"y":52595660,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":184,"mMastLocX":75},{"lid":"A=1@O=Berlin-Wittenau (Wilhelmsruher Damm)@X=13334446@Y=52597045@U=80@L=8089096@","type":"S","name":"Berlin-Wittenau (Wilhelmsruher Damm)","icoX":0,"extId":"8089096","state":"F","crd":{"x":13334868,"y":52596784,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":184},{"lid":"A=1@O=Lindauer Allee (U), Berlin@X=13339039@Y=52575390@U=80@L=732319@","type":"S","name":"Lindauer Allee (U), Berlin","icoX":0,"extId":"732319","state":"F","crd":{"x":13339039,"y":52575390,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Karl-Bonhoeffer-Nervenklinik (S+U), Berlin@X=13333115@Y=52578707@U=80@L=732520@","type":"S","name":"Karl-Bonhoeffer-Nervenklinik (S+U), Berlin","icoX":0,"extId":"732520","state":"F","crd":{"x":13333115,"y":52578707,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":176,"mMastLocX":78},{"lid":"A=1@O=Berlin Karl-Bonhoeffer-Nervenklinik@X=13329178@Y=52578050@U=80@L=8089102@","type":"S","name":"Berlin Karl-Bonhoeffer-Nervenklinik","icoX":3,"extId":"8089102","state":"F","crd":{"x":13329349,"y":52578050,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":176},{"lid":"A=1@O=Rathaus Reinickendorf (U), Berlin@X=13325915@Y=52588388@U=80@L=732512@","type":"S","name":"Rathaus Reinickendorf (U), Berlin","icoX":0,"extId":"732512","state":"F","crd":{"x":13325915,"y":52588388,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":160},{"lid":"A=1@O=Warschauer Str. (S+U), Berlin@X=13449112@Y=52505238@U=80@L=732675@","type":"S","name":"Warschauer Str. (S+U), Berlin","icoX":0,"extId":"732675","state":"F","crd":{"x":13449112,"y":52505238,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":432,"mMastLocX":61},{"lid":"A=1@O=Holzmarktstr., Berlin@X=13423771@Y=52513634@U=80@L=732596@","type":"S","name":"Holzmarktstr., Berlin","icoX":0,"extId":"732596","state":"F","crd":{"x":13423771,"y":52513634,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=Stralauer Platz, Berlin@X=13430729@Y=52511027@U=80@L=732691@","type":"S","name":"Stralauer Platz, Berlin","icoX":0,"extId":"732691","state":"F","crd":{"x":13430729,"y":52511027,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=Ostbahnhof (S), Berlin@X=13432410@Y=52510353@U=80@L=732676@","type":"S","name":"Ostbahnhof (S), Berlin","icoX":0,"extId":"732676","state":"F","crd":{"x":13432410,"y":52510353,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":63,"mMastLocX":60},{"lid":"A=1@O=Rummelsburger Platz, Berlin@X=13437138@Y=52506730@U=80@L=220546@","type":"S","name":"Rummelsburger Platz, Berlin","icoX":0,"extId":"220546","state":"F","crd":{"x":13437138,"y":52506730,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=East Side Gallery, Berlin@X=13438379@Y=52505957@U=80@L=728701@","type":"S","name":"East Side Gallery, Berlin","icoX":0,"extId":"728701","state":"F","crd":{"x":13438379,"y":52505957,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=Tamara-Danz-Str., Berlin@X=13444537@Y=52503503@U=80@L=728700@","type":"S","name":"Tamara-Danz-Str., Berlin","icoX":0,"extId":"728700","state":"F","crd":{"x":13444537,"y":52503503,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":32},{"lid":"A=1@O=Ahrensfelde (S)@X=13565549@Y=52571246@U=80@L=8089188@","type":"S","name":"Ahrensfelde (S)","icoX":3,"extId":"8089188","state":"F","crd":{"x":13565549,"y":52571246,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56,"entry":true,"mMastLocX":88},{"lid":"A=1@O=Ahrensfelde@X=13565154@Y=52571371@U=80@L=8011003@","type":"S","name":"Ahrensfelde","icoX":0,"extId":"8011003","state":"F","crd":{"x":13566008,"y":52572306,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Berlin Nöldnerplatz@X=13484449@Y=52503494@U=80@L=8089026@","type":"S","name":"Berlin Nöldnerplatz","icoX":3,"extId":"8089026","state":"F","crd":{"x":13485366,"y":52503817,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin-Lichtenberg (S)@X=13497249@Y=52509921@U=80@L=8089182@","type":"S","name":"Berlin-Lichtenberg (S)","icoX":3,"extId":"8089182","state":"F","crd":{"x":13497249,"y":52509921,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":447,"entry":true,"mMastLocX":91},{"lid":"A=1@O=Berlin-Lichtenberg@X=13496692@Y=52510137@U=80@L=8010036@","type":"S","name":"Berlin-Lichtenberg","icoX":9,"extId":"8010036","state":"F","crd":{"x":13496494,"y":52509840,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":447},{"lid":"A=1@O=Berlin-Friedrichsfelde Ost@X=13520253@Y=52514173@U=80@L=8089059@","type":"S","name":"Berlin-Friedrichsfelde Ost","icoX":0,"extId":"8089059","state":"F","crd":{"x":13520091,"y":52514128,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312},{"lid":"A=1@O=Berlin Springpfuhl@X=13536739@Y=52526344@U=80@L=8089040@","type":"S","name":"Berlin Springpfuhl","icoX":3,"extId":"8089040","state":"F","crd":{"x":13536883,"y":52526389,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":304},{"lid":"A=1@O=Berlin Poelchaustr.@X=13535382@Y=52535594@U=80@L=8089031@","type":"S","name":"Berlin Poelchaustr.","icoX":3,"extId":"8089031","state":"F","crd":{"x":13535642,"y":52535855,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin-Marzahn@X=13541341@Y=52543604@U=80@L=8089075@","type":"S","name":"Berlin-Marzahn","icoX":0,"extId":"8089075","state":"F","crd":{"x":13540865,"y":52542992,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312},{"lid":"A=1@O=Berlin Raoul-Wallenberg-Str.@X=13547463@Y=52550678@U=80@L=8089035@","type":"S","name":"Berlin Raoul-Wallenberg-Str.","icoX":0,"extId":"8089035","state":"F","crd":{"x":13547616,"y":52550669,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Berlin Mehrower Allee@X=13553702@Y=52557762@U=80@L=8089022@","type":"S","name":"Berlin Mehrower Allee","icoX":0,"extId":"8089022","state":"F","crd":{"x":13553558,"y":52557564,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Potsdam Hbf (S)@X=13066711@Y=52391857@U=80@L=8089184@","type":"S","name":"Potsdam Hbf (S)","icoX":3,"extId":"8089184","state":"F","crd":{"x":13067727,"y":52391713,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":315,"entry":true,"mMastLocX":99},{"lid":"A=1@O=Potsdam Hbf@X=13066702@Y=52391506@U=80@L=8012666@","type":"S","name":"Potsdam Hbf","icoX":6,"extId":"8012666","state":"F","crd":{"x":13066711,"y":52391551,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":315},{"lid":"A=1@O=Berlin-Grunewald@X=13261723@Y=52488689@U=80@L=8089062@","type":"S","name":"Berlin-Grunewald","icoX":3,"extId":"8089062","state":"F","crd":{"x":13261831,"y":52488680,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin-Nikolassee@X=13193639@Y=52431805@U=80@L=8089078@","type":"S","name":"Berlin-Nikolassee","icoX":3,"extId":"8089078","state":"F","crd":{"x":13193270,"y":52432425,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin Wannsee (S)@X=13179418@Y=52421180@U=80@L=8089190@","type":"S","name":"Berlin Wannsee (S)","icoX":3,"extId":"8089190","state":"F","crd":{"x":13179418,"y":52421180,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":123,"entry":true,"mMastLocX":103},{"lid":"A=1@O=Berlin Wannsee@X=13179526@Y=52420973@U=80@L=8010405@","type":"S","name":"Berlin Wannsee","icoX":6,"extId":"8010405","state":"F","crd":{"x":13179696,"y":52420955,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":123},{"lid":"A=1@O=Potsdam Griebnitzsee (S)@X=13128943@Y=52394473@U=80@L=8080530@","type":"S","name":"Potsdam Griebnitzsee (S)","icoX":3,"extId":"8080530","state":"F","crd":{"x":13128638,"y":52394392,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56,"entry":true,"mMastLocX":105},{"lid":"A=1@O=Potsdam Griebnitzsee@X=13128728@Y=52394437@U=80@L=8011695@","type":"S","name":"Potsdam Griebnitzsee","icoX":0,"extId":"8011695","state":"F","crd":{"x":13128728,"y":52394437,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Potsdam-Babelsberg@X=13092870@Y=52391389@U=80@L=8080070@","type":"S","name":"Potsdam-Babelsberg","icoX":0,"extId":"8080070","state":"F","crd":{"x":13093049,"y":52391353,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312},{"lid":"A=1@O=Strausberg Nord@X=13908479@Y=52590150@U=80@L=8013064@","type":"S","name":"Strausberg Nord","icoX":0,"extId":"8013064","state":"F","crd":{"x":13909153,"y":52590905,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":568},{"lid":"A=1@O=Berlin-Biesdorf@X=13555877@Y=52513094@U=80@L=8089055@","type":"S","name":"Berlin-Biesdorf","icoX":3,"extId":"8089055","state":"F","crd":{"x":13556254,"y":52513085,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin Wuhletal@X=13575366@Y=52512420@U=80@L=8089049@","type":"S","name":"Berlin Wuhletal","icoX":0,"extId":"8089049","state":"F","crd":{"x":13574673,"y":52512465,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":184},{"lid":"A=1@O=Berlin-Kaulsdorf@X=13588939@Y=52512096@U=80@L=8089068@","type":"S","name":"Berlin-Kaulsdorf","icoX":3,"extId":"8089068","state":"F","crd":{"x":13590198,"y":52512079,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":48},{"lid":"A=1@O=Berlin-Mahlsdorf (S)@X=13611071@Y=52512159@U=80@L=8089072@","type":"S","name":"Berlin-Mahlsdorf (S)","icoX":0,"extId":"8089072","state":"F","crd":{"x":13611907,"y":52512249,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312,"entry":true,"mMastLocX":112},{"lid":"A=1@O=Berlin-Mahlsdorf@X=13611421@Y=52512105@U=80@L=8011343@","type":"S","name":"Berlin-Mahlsdorf","icoX":0,"extId":"8011343","state":"F","crd":{"x":13611421,"y":52512105,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312},{"lid":"A=1@O=Birkenstein@X=13647189@Y=52515602@U=80@L=8070002@","type":"S","name":"Birkenstein","icoX":0,"extId":"8070002","state":"F","crd":{"x":13647459,"y":52515611,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Hoppegarten(Mark)@X=13672952@Y=52518119@U=80@L=8080750@","type":"S","name":"Hoppegarten(Mark)","icoX":0,"extId":"8080750","state":"F","crd":{"x":13673087,"y":52518056,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Neuenhagen(b Berlin)@X=13700423@Y=52520762@U=80@L=8081020@","type":"S","name":"Neuenhagen(b Berlin)","icoX":0,"extId":"8081020","state":"F","crd":{"x":13700882,"y":52520807,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Fredersdorf(b Berlin)@X=13760624@Y=52526210@U=80@L=8080440@","type":"S","name":"Fredersdorf(b Berlin)","icoX":0,"extId":"8080440","state":"F","crd":{"x":13762638,"y":52526398,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Petershagen Nord@X=13789381@Y=52528906@U=80@L=8081150@","type":"S","name":"Petershagen Nord","icoX":3,"extId":"8081150","state":"F","crd":{"x":13789165,"y":52528861,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":560},{"lid":"A=1@O=Strausberg (S)@X=13833581@Y=52532232@U=80@L=8089187@","type":"S","name":"Strausberg (S)","icoX":3,"extId":"8089187","state":"F","crd":{"x":13833581,"y":52532232,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":824,"entry":true,"mMastLocX":119},{"lid":"A=1@O=Strausberg@X=13833500@Y=52532080@U=80@L=8010341@","type":"S","name":"Strausberg","icoX":0,"extId":"8010341","state":"F","crd":{"x":13835621,"y":52532026,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":824},{"lid":"A=1@O=Strausberg-Hegermühle@X=13866607@Y=52548377@U=80@L=8080680@","type":"S","name":"Strausberg-Hegermühle","icoX":3,"extId":"8080680","state":"F","crd":{"x":13866733,"y":52548799,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":16},{"lid":"A=1@O=Strausberg Stadt@X=13888136@Y=52576981@U=80@L=8081420@","type":"S","name":"Strausberg Stadt","icoX":0,"extId":"8081420","state":"F","crd":{"x":13887903,"y":52576540,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":568},{"lid":"A=1@O=Flughafen BER - Terminal 1-2 (S-Bahn)@X=13511920@Y=52364844@U=80@L=8089201@","type":"S","name":"Flughafen BER - Terminal 1-2 (S-Bahn)","icoX":3,"extId":"8089201","state":"F","crd":{"x":13511920,"y":52364844,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":58,"entry":true,"mMastLocX":123},{"lid":"A=1@O=Flughafen BER - Terminal 1-2@X=13511947@Y=52364808@U=80@L=8011201@","type":"S","name":"Flughafen BER - Terminal 1-2","icoX":10,"extId":"8011201","state":"F","crd":{"x":13511947,"y":52364808,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":58},{"lid":"A=1@O=Berlin Treptower Park@X=13461445@Y=52493561@U=80@L=8089043@","type":"S","name":"Berlin Treptower Park","icoX":0,"extId":"8089043","state":"F","crd":{"x":13461364,"y":52493345,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Berlin Plänterwald@X=13473122@Y=52478540@U=80@L=8089030@","type":"S","name":"Berlin Plänterwald","icoX":0,"extId":"8089030","state":"F","crd":{"x":13473374,"y":52478477,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Berlin Baumschulenweg@X=13490094@Y=52467240@U=80@L=8089004@","type":"S","name":"Berlin Baumschulenweg","icoX":0,"extId":"8089004","state":"F","crd":{"x":13486300,"y":52469470,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Berlin-Schöneweide (S)@X=13509115@Y=52454979@U=80@L=8089168@","type":"S","name":"Berlin-Schöneweide (S)","icoX":3,"extId":"8089168","state":"F","crd":{"x":13509115,"y":52454979,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312,"entry":true,"mMastLocX":128},{"lid":"A=1@O=Berlin-Schöneweide@X=13509394@Y=52455123@U=80@L=8010041@","type":"S","name":"Berlin-Schöneweide","icoX":0,"extId":"8010041","state":"F","crd":{"x":13508773,"y":52455204,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312},{"lid":"A=1@O=Berlin-Johannisthal@X=13523678@Y=52446907@U=80@L=8089007@","type":"S","name":"Berlin-Johannisthal","icoX":0,"extId":"8089007","state":"F","crd":{"x":13523705,"y":52446907,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":24},{"lid":"A=1@O=Berlin-Adlershof@X=13541126@Y=52435032@U=80@L=8089052@","type":"S","name":"Berlin-Adlershof","icoX":0,"extId":"8089052","state":"F","crd":{"x":13541386,"y":52434816,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":312},{"lid":"A=1@O=Berlin-Altglienicke@X=13558753@Y=52407300@U=80@L=8089054@","type":"S","name":"Berlin-Altglienicke","icoX":0,"extId":"8089054","state":"F","crd":{"x":13559365,"y":52407714,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Berlin Grünbergallee@X=13542483@Y=52399408@U=80@L=8089016@","type":"S","name":"Berlin Grünbergallee","icoX":0,"extId":"8089016","state":"F","crd":{"x":13542555,"y":52399480,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":24},{"lid":"A=1@O=Flughafen BER - Terminal 5 (Schönefeld)@X=13512711@Y=52391066@U=80@L=8010109@","type":"S","name":"Flughafen BER - Terminal 5 (Schönefeld)","icoX":0,"extId":"8010109","state":"F","crd":{"x":13513196,"y":52391677,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56},{"lid":"A=1@O=Waßmannsdorf@X=13463531@Y=52368422@U=80@L=8013241@","type":"S","name":"Waßmannsdorf","icoX":0,"extId":"8013241","state":"F","crd":{"x":13463926,"y":52368898,"z":0,"type":"WGS84","layerX":0,"crdSysX":0},"pCls":56}],"prodL":[{"name":"Bus 300","nameS":"300","number":"300","icoX":0,"cls":32,"oprX":0,"prodCtx":{"name":"Bus 300","num":"50833","line":"300","lineId":"5_vbbBVB_300","matchId":"300","catOut":"Bus","catOutS":"Bus","catOutL":"Bus","catIn":"Bus","catCode":"5","admin":"vbbBVB"}},{"name":"Bus N8","nameS":"N8","icoX":0,"cls":32,"prodCtx":{"name":"Bus N8","line":"N8","lineId":"5_vbbBVB_N8","matchId":"","catOut":"Bus ","catOutS":"Bus","catOutL":"Bus"}},{"name":"Bus 300","nameS":"300","icoX":0,"cls":32,"prodCtx":{"name":"Bus 300","line":"300","lineId":"5_vbbBVB_300","matchId":"","catOut":"Bus ","catOutS":"Bus","catOutL":"Bus"}},{"name":"Bus N40","nameS":"N40","icoX":0,"cls":32,"prodCtx":{"name":"Bus N40","line":"N40","lineId":"5_vbbBVB_N40","matchId":"","catOut":"Bus ","catOutS":"Bus","catOutL":"Bus"}},{"name":"Bus N60","nameS":"N60","icoX":0,"cls":32,"prodCtx":{"name":"Bus N60","line":"N60","lineId":"5_vbbBVB_N60","matchId":"","catOut":"Bus ","catOutS":"Bus","catOutL":"Bus"}},{"name":"Bus N65","nameS":"N65","icoX":0,"cls":32,"prodCtx":{"name":"Bus N65","line":"N65","lineId":"5_vbbBVB_N65","matchId":"","catOut":"Bus ","catOutS":"Bus","catOutL":"Bus"}},{"name":"U 8","nameS":"8","icoX":2,"cls":128,"prodCtx":{"name":"U 8","line":"8","lineId":"7_vbbBVU_8","matchId":"","catOut":"U ","catOutS":"U","catOutL":"U-Bahn"}},{"name":"Bus SEV","nameS":"SEV","icoX":0,"cls":8,"prodCtx":{"name":"Bus SEV","line":"SEV","lineId":"3_080001_SEV!!1498363!!5840671","matchId":"","catOut":"Bus ","catOutS":"Bsv","catOutL":"SEV-Bus"}},{"name":"S 3","nameS":"3","icoX":3,"cls":16,"prodCtx":{"name":"S 3","line":"3","lineId":"4_08_____3","matchId":"","catOut":"S ","catOutS":"s","catOutL":"S-Bahn"}},{"name":"S 5","nameS":"5","icoX":3,"cls":16,"prodCtx":{"name":"S 5","line":"5","lineId":"4_08_____5","matchId":"","catOut":"S ","catOutS":"s","catOutL":"S-Bahn"}},{"name":"S 7","nameS":"7","icoX":3,"cls":16,"prodCtx":{"name":"S 7","line":"7","lineId":"4_08_____7","matchId":"","catOut":"S ","catOutS":"s","catOutL":"S-Bahn"}},{"name":"S 9","nameS":"9","icoX":3,"cls":16,"prodCtx":{"name":"S 9","line":"9","lineId":"4_08_____9","matchId":"","catOut":"S ","catOutS":"s","catOutL":"S-Bahn"}},{"name":"S 47","nameS":"47","icoX":3,"cls":16,"prodCtx":{"name":"S 47","line":"47","lineId":"4_08_____47","matchId":"","catOut":"S ","catOutS":"s","catOutL":"S-Bahn"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"19458","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"20024","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"S 3","nameS":"3","number":"3","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 3","num":"3122","line":"3","lineId":"4_08_____3","matchId":"3","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 3","nameS":"3","number":"3","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 3","num":"3127","line":"3","lineId":"4_08_____3","matchId":"3","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 5","nameS":"5","number":"5","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 5","num":"5100","line":"5","lineId":"4_08_____5","matchId":"5","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"19326","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"20168","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"Bus 300","nameS":"300","number":"300","icoX":0,"cls":32,"oprX":0,"prodCtx":{"name":"Bus 300","num":"50749","line":"300","lineId":"5_vbbBVB_300","matchId":"300","catOut":"Bus","catOutS":"Bus","catOutL":"Bus","catIn":"Bus","catCode":"5","admin":"vbbBVB"}},{"name":"S 7","nameS":"7","number":"7","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 7","num":"7117","line":"7","lineId":"4_08_____7","matchId":"7","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 7","nameS":"7","number":"7","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 7","num":"7584","line":"7","lineId":"4_08_____7","matchId":"7","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 5","nameS":"5","number":"5","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 5","num":"5105","line":"5","lineId":"4_08_____5","matchId":"5","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"19459","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"20026","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"S 9","nameS":"9","number":"9","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 9","num":"9106","line":"9","lineId":"4_08_____9","matchId":"9","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 9","nameS":"9","number":"9","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 9","num":"9107","line":"9","lineId":"4_08_____9","matchId":"9","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 5","nameS":"5","number":"5","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 5","num":"5590","line":"5","lineId":"4_08_____5","matchId":"5","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"19328","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"20169","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"S 7","nameS":"7","number":"7","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 7","num":"7583","line":"7","lineId":"4_08_____7","matchId":"7","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 7","nameS":"7","number":"7","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 7","num":"7118","line":"7","lineId":"4_08_____7","matchId":"7","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 5","nameS":"5","number":"5","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 5","num":"55591","line":"5","lineId":"4_08_____5","matchId":"5","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"19460","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"U 8","nameS":"8","number":"8","icoX":2,"cls":128,"oprX":0,"prodCtx":{"name":"U 8","num":"20028","line":"8","lineId":"7_vbbBVU_8","matchId":"8","catOut":"U","catOutS":"U","catOutL":"U-Bahn","catIn":"U","catCode":"7","admin":"vbbBVU"}},{"name":"S 3","nameS":"3","number":"3","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 3","num":"3124","line":"3","lineId":"4_08_____3","matchId":"3","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 3","nameS":"3","number":"3","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 3","num":"3129","line":"3","lineId":"4_08_____3","matchId":"3","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}},{"name":"S 5","nameS":"5","number":"5","icoX":3,"cls":16,"oprX":1,"prodCtx":{"name":"S 5","num":"5102","line":"5","lineId":"4_08_____5","matchId":"5","catOut":"S","catOutS":"s","catOutL":"S-Bahn","catIn":"s","catCode":"4","admin":"08____"}}],"polyL":[],"layerL":[{"id":"standard","name":"standard","index":0,"annoCnt":0}],"crdSysL":[{"id":"standard","index":0,"type":"WGS84","dim":3}],"opL":[{"name":"Nahreisezug","icoX":1},{"name":"S-Bahn Berlin","icoX":7}],"remL":[{"type":"P","code":"","icoX":11,"txtN":"Fahrt fällt aus"}],"icoL":[{"res":"Bus"},{"res":"DPN","txt":"Nahreisezug"},{"res":"U"},{"res":"S"},{"res":"rt_cnf"},{"res":"rt_ont"},{"res":"ICE"},{"res":"s","txt":"S-Bahn Berlin"},{"res":"D"},{"res":"RJ"},{"res":"IC"},{"res":"cancel"}]},"type":"DEP","jnyL":[{"jid":"1|1137745|35|80|2102022","date":"20221002","prodX":0,"dirTxt":"Tiergarten, Philharmonie","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":7,"dProdX":0,"dInR":true,"dTimeS":"165500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":7,"dInR":true,"dTimeS":"165500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":3,"idx":8,"aOutR":true,"aTimeS":"165600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":4,"idx":9,"aOutR":true,"aTimeS":"165800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":6,"idx":10,"aOutR":true,"aTimeS":"170000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":7,"idx":11,"aOutR":true,"aTimeS":"170200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":8,"idx":12,"aOutR":true,"aTimeS":"170400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":9,"idx":13,"aOutR":true,"aTimeS":"170500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":10,"idx":14,"aOutR":true,"aTimeS":"170900","aTZOffset":120,"type":"N"},{"locX":11,"idx":15,"aOutR":true,"aTimeS":"171000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":12,"idx":16,"aOutR":true,"aTimeS":"171200","aTZOffset":120,"type":"N"},{"locX":13,"idx":17,"aOutR":true,"aTimeS":"171300","aTZOffset":120,"type":"N"},{"locX":15,"idx":18,"aOutR":true,"aTimeS":"171500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":2,"idx":19,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13433363,"y":52509624,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1085848|26|80|2102022","date":"20221002","prodX":13,"dirTxt":"Paracelsus-Bad (U), Berlin","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":8,"dProdX":13,"dInR":true,"dTimeS":"170000","dTimeR":"170000","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":8,"dInR":true,"dTimeS":"170000","dTimeR":"170000","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":17,"idx":9,"aOutR":true,"aTimeS":"170200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":18,"idx":10,"aOutR":true,"aTimeS":"170400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":19,"idx":11,"aOutR":true,"aTimeS":"170500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":20,"idx":12,"aOutR":true,"aTimeS":"170700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":21,"idx":13,"aOutR":true,"aTimeS":"170800","aTZOffset":120,"type":"N"},{"locX":22,"idx":14,"aOutR":true,"aTimeS":"171000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":24,"idx":15,"aOutR":true,"aTimeS":"171300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":25,"idx":16,"aOutR":true,"aTimeS":"171500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":26,"idx":17,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":16,"idx":18,"aOutR":true,"aTimeS":"171700","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13416742,"y":52516915,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1086924|29|80|2102022","date":"20221002","prodX":14,"dirTxt":"Hermannstr. (S+U), Berlin","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":15,"dProdX":14,"dInR":true,"dTimeS":"170000","dTimeR":"170000","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":15,"dInR":true,"dTimeS":"170000","dTimeR":"170000","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},{"locX":29,"idx":16,"aOutR":true,"aTimeS":"170100","aTZOffset":120,"type":"N"},{"locX":30,"idx":17,"aOutR":true,"aTimeS":"170300","aTZOffset":120,"type":"N"},{"locX":31,"idx":18,"aOutR":true,"aTimeS":"170500","aTZOffset":120,"type":"N"},{"locX":32,"idx":19,"aOutR":true,"aTimeS":"170600","aTZOffset":120,"type":"N"},{"locX":33,"idx":20,"aOutR":true,"aTimeS":"170800","aTZOffset":120,"type":"N"},{"locX":34,"idx":21,"aOutR":true,"aTimeS":"171000","aTZOffset":120,"type":"N"},{"locX":35,"idx":22,"aOutR":true,"aTimeS":"171100","aTZOffset":120,"type":"N"},{"locX":27,"idx":23,"aOutR":true,"aTimeS":"171200","aTZOffset":120,"type":"N"}],"pos":{"x":13417048,"y":52512780,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|296882|3|80|2102022","date":"20221002","prodX":15,"dirTxt":"Berlin-Spandau (S)","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":13,"dProdX":15,"dPlatfS":"4","dInR":true,"dTimeS":"170100","dTimeR":"170100","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":13,"dPlatfS":"4","dInR":true,"dTimeS":"170100","dTimeR":"170100","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":38,"idx":14,"aOutR":true,"aTimeS":"170200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":39,"idx":15,"aOutR":true,"aTimeS":"170400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":40,"idx":16,"aOutR":true,"aTimeS":"170600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":42,"idx":17,"aOutR":true,"aTimeS":"170900","aTZOffset":120,"type":"N"},{"locX":44,"idx":18,"aOutR":true,"aTimeS":"171200","aTZOffset":120,"type":"N"},{"locX":45,"idx":19,"aOutR":true,"aTimeS":"171400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":46,"idx":20,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":48,"idx":21,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":49,"idx":22,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":51,"idx":23,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":52,"idx":24,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"type":"N"},{"locX":53,"idx":25,"aOutR":true,"aTimeS":"172700","aTZOffset":120,"type":"N"},{"locX":54,"idx":26,"aOutR":true,"aTimeS":"172900","aTZOffset":120,"type":"N"},{"locX":55,"idx":27,"aOutR":true,"aTimeS":"173100","aTZOffset":120,"type":"N"},{"locX":56,"idx":28,"aOutR":true,"aTimeS":"173500","aTZOffset":120,"type":"N"},{"locX":36,"idx":29,"aOutR":true,"aTimeS":"173700","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13419681,"y":52514227,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|296884|2|80|2102022","date":"20221002","prodX":16,"dirTxt":"Erkner (S)","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":16,"dProdX":16,"dPlatfS":"3","dInR":true,"dTimeS":"170400","dTimeR":"170400","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":16,"dPlatfS":"3","dInR":true,"dTimeS":"170400","dTimeR":"170400","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":59,"idx":17,"aOutR":true,"aTimeS":"170600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":61,"idx":18,"aOutR":true,"aTimeS":"170900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":62,"idx":19,"aOutR":true,"aTimeS":"171100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":64,"idx":20,"aOutR":true,"aTimeS":"171300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":65,"idx":21,"aOutR":true,"aTimeS":"171500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":66,"idx":22,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":68,"idx":23,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"type":"N"},{"locX":69,"idx":24,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":70,"idx":25,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"type":"N"},{"locX":71,"idx":26,"aOutR":true,"aTimeS":"172900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":72,"idx":27,"aOutR":true,"aTimeS":"173300","aTZOffset":120,"type":"N"},{"locX":73,"idx":28,"aOutR":true,"aTimeS":"173700","aTZOffset":120,"type":"N"},{"locX":57,"idx":29,"aOutR":true,"aTimeS":"174000","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13407564,"y":52523243,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|301817|1|80|2102022","date":"20221002","prodX":17,"dirTxt":"Berlin Olympiastadion","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":19,"dProdX":17,"dPlatfS":"4","dInR":true,"dTimeS":"170500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":19,"dPlatfS":"4","dInR":true,"dTimeS":"170500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":38,"idx":20,"aOutR":true,"aTimeS":"170700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":39,"idx":21,"aOutR":true,"aTimeS":"170900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":40,"idx":22,"aOutR":true,"aTimeS":"171100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":42,"idx":23,"aOutR":true,"aTimeS":"171400","aTZOffset":120,"type":"N"},{"locX":44,"idx":24,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"type":"N"},{"locX":45,"idx":25,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":46,"idx":26,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":48,"idx":27,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":49,"idx":28,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":51,"idx":29,"aOutR":true,"aTimeS":"172700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":52,"idx":30,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"type":"N"},{"locX":53,"idx":31,"aOutR":true,"aTimeS":"173300","aTZOffset":120,"type":"N"},{"locX":54,"idx":32,"aOutR":true,"aTimeS":"173700","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13452240,"y":52505948,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1085334|62|80|2102022","date":"20221002","prodX":18,"dirTxt":"S+U Wittenau","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":8,"dProdX":18,"dInR":true,"dTimeS":"170500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":8,"dInR":true,"dTimeS":"170500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":17,"idx":9,"aOutR":true,"aTimeS":"170700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":18,"idx":10,"aOutR":true,"aTimeS":"170900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":19,"idx":11,"aOutR":true,"aTimeS":"171000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":20,"idx":12,"aOutR":true,"aTimeS":"171200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":21,"idx":13,"aOutR":true,"aTimeS":"171300","aTZOffset":120,"type":"N"},{"locX":22,"idx":14,"aOutR":true,"aTimeS":"171500","aTZOffset":120,"type":"N"},{"locX":24,"idx":15,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":25,"idx":16,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"type":"N"},{"locX":26,"idx":17,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":16,"idx":18,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":76,"idx":19,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"type":"N"},{"locX":77,"idx":20,"aOutR":true,"aTimeS":"172500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":79,"idx":21,"aOutR":true,"aTimeS":"172700","aTZOffset":120,"type":"N"},{"locX":74,"idx":22,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13418935,"y":52497929,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1087305|22|80|2102022","date":"20221002","prodX":19,"dirTxt":"Hermannstr. (S+U), Berlin","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":11,"dProdX":19,"dInR":true,"dTimeS":"170500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":11,"dInR":true,"dTimeS":"170500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},{"locX":29,"idx":12,"aOutR":true,"aTimeS":"170600","aTZOffset":120,"type":"N"},{"locX":30,"idx":13,"aOutR":true,"aTimeS":"170800","aTZOffset":120,"type":"N"},{"locX":31,"idx":14,"aOutR":true,"aTimeS":"171000","aTZOffset":120,"type":"N"},{"locX":32,"idx":15,"aOutR":true,"aTimeS":"171100","aTZOffset":120,"type":"N"},{"locX":33,"idx":16,"aOutR":true,"aTimeS":"171300","aTZOffset":120,"type":"N"},{"locX":34,"idx":17,"aOutR":true,"aTimeS":"171500","aTZOffset":120,"type":"N"},{"locX":35,"idx":18,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"type":"N"},{"locX":27,"idx":19,"aOutR":true,"aTimeS":"171700","aTZOffset":120,"type":"N"}],"pos":{"x":13403582,"y":52527315,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1137745|36|80|2102022","date":"20221002","prodX":0,"dirTxt":"Tiergarten, Philharmonie","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":7,"dProdX":0,"dInR":true,"dTimeS":"170500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":7,"dInR":true,"dTimeS":"170500","dTimeR":"170500","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":3,"idx":8,"aOutR":true,"aTimeS":"170600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":4,"idx":9,"aOutR":true,"aTimeS":"170800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":6,"idx":10,"aOutR":true,"aTimeS":"171000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":7,"idx":11,"aOutR":true,"aTimeS":"171200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":8,"idx":12,"aOutR":true,"aTimeS":"171400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":9,"idx":13,"aOutR":true,"aTimeS":"171500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":10,"idx":14,"aOutR":true,"aTimeS":"171900","aTZOffset":120,"type":"N"},{"locX":11,"idx":15,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":12,"idx":16,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"type":"N"},{"locX":13,"idx":17,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"type":"N"},{"locX":15,"idx":18,"aOutR":true,"aTimeS":"172500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":2,"idx":19,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13433363,"y":52509624,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1507932|34|80|2102022","date":"20221002","prodX":20,"dirTxt":"S+U Warschauer Str.","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":11,"dProdX":20,"dInR":true,"dTimeS":"170600","dTimeR":"171200","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":11,"dInR":true,"dTimeS":"170600","dTimeR":"171200","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},{"locX":81,"idx":12,"aOutR":true,"aTimeS":"170700","aTZOffset":120,"type":"N"},{"locX":82,"idx":13,"aOutR":true,"aTimeS":"170900","aTZOffset":120,"type":"N"},{"locX":83,"idx":14,"aOutR":true,"aTimeS":"171000","aTZOffset":120,"type":"N"},{"locX":84,"idx":15,"aOutR":true,"aTimeS":"171200","aTZOffset":120,"type":"N"},{"locX":85,"idx":16,"aOutR":true,"aTimeS":"171400","aTZOffset":120,"type":"N"},{"locX":86,"idx":17,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"type":"N"},{"locX":80,"idx":18,"aOutR":true,"aTimeS":"171700","aTZOffset":120,"type":"N"}],"pos":{"x":13388696,"y":52516870,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|303933|2|80|2102022","date":"20221002","prodX":21,"dirTxt":"Ahrensfelde (S)","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":16,"dProdX":21,"dPlatfS":"3","dInR":true,"dTimeS":"170700","dTimeR":"170700","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":16,"dPlatfS":"3","dInR":true,"dTimeS":"170700","dTimeR":"170700","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":59,"idx":17,"aOutR":true,"aTimeS":"170900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":61,"idx":18,"aOutR":true,"aTimeS":"171100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":62,"idx":19,"aOutR":true,"aTimeS":"171300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":89,"idx":20,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":90,"idx":21,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":92,"idx":22,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":93,"idx":23,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":94,"idx":24,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"type":"N"},{"locX":95,"idx":25,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":96,"idx":26,"aOutR":true,"aTimeS":"172900","aTZOffset":120,"type":"N"},{"locX":97,"idx":27,"aOutR":true,"aTimeS":"173100","aTZOffset":120,"type":"N"},{"locX":87,"idx":28,"aOutR":true,"aTimeS":"173400","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13386322,"y":52520555,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|304775|2|80|2102022","date":"20221002","prodX":22,"dirTxt":"Potsdam Hbf (S)","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":12,"dProdX":22,"dPlatfS":"4","dInR":true,"dTimeS":"170800","dTimeR":"170800","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":12,"dPlatfS":"4","dInR":true,"dTimeS":"170800","dTimeR":"170800","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":38,"idx":13,"aOutR":true,"aTimeS":"170900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":39,"idx":14,"aOutR":true,"aTimeS":"171100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":40,"idx":15,"aOutR":true,"aTimeS":"171300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":42,"idx":16,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"type":"N"},{"locX":44,"idx":17,"aOutR":true,"aTimeS":"171900","aTZOffset":120,"type":"N"},{"locX":45,"idx":18,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":46,"idx":19,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":48,"idx":20,"aOutR":true,"aTimeS":"172500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":49,"idx":21,"aOutR":true,"aTimeS":"172700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":51,"idx":22,"aOutR":true,"aTimeS":"172900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":100,"idx":23,"aOutR":true,"aTimeS":"173200","aTZOffset":120,"type":"N"},{"locX":101,"idx":24,"aOutR":true,"aTimeS":"174000","aTZOffset":120,"type":"N"},{"locX":102,"idx":25,"aOutR":true,"aTimeS":"174200","aTZOffset":120,"type":"N"},{"locX":104,"idx":26,"aOutR":true,"aTimeS":"174700","aTZOffset":120,"type":"N"},{"locX":106,"idx":27,"aOutR":true,"aTimeS":"175000","aTZOffset":120,"type":"N"},{"locX":98,"idx":28,"aOutR":true,"aTimeS":"175200","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13468771,"y":52503107,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|301834|0|80|2102022","date":"20221002","prodX":23,"dirTxt":"Strausberg Nord","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":12,"dProdX":23,"dPlatfS":"3","dInR":true,"dTimeS":"170900","dTimeR":"170900","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":12,"dPlatfS":"3","dInR":true,"dTimeS":"170900","dTimeR":"170900","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":59,"idx":13,"aOutR":true,"aTimeS":"171100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":61,"idx":14,"aOutR":true,"aTimeS":"171400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":62,"idx":15,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":89,"idx":16,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":90,"idx":17,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":92,"idx":18,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":108,"idx":19,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"type":"N"},{"locX":109,"idx":20,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":110,"idx":21,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"type":"N"},{"locX":111,"idx":22,"aOutR":true,"aTimeS":"173200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":113,"idx":23,"aOutR":true,"aTimeS":"173600","aTZOffset":120,"type":"N"},{"locX":114,"idx":24,"aOutR":true,"aTimeS":"173900","aTZOffset":120,"type":"N"},{"locX":115,"idx":25,"aOutR":true,"aTimeS":"174100","aTZOffset":120,"type":"N"},{"locX":116,"idx":26,"aOutR":true,"aTimeS":"174600","aTZOffset":120,"type":"N"},{"locX":117,"idx":27,"aOutR":true,"aTimeS":"174900","aTZOffset":120,"type":"N"},{"locX":118,"idx":28,"aOutR":true,"aTimeS":"175300","aTZOffset":120,"type":"N"},{"locX":120,"idx":29,"aOutR":true,"aTimeS":"175800","aTZOffset":120,"type":"N"},{"locX":121,"idx":30,"aOutR":true,"aTimeS":"180200","aTZOffset":120,"type":"N"},{"locX":107,"idx":31,"aOutR":true,"aTimeS":"180400","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13369333,"y":52525185,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1085848|27|80|2102022","date":"20221002","prodX":24,"dirTxt":"Paracelsus-Bad (U), Berlin","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":8,"dProdX":24,"dInR":true,"dTimeS":"171000","dTimeR":"171000","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":8,"dInR":true,"dTimeS":"171000","dTimeR":"171000","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":17,"idx":9,"aOutR":true,"aTimeS":"171200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":18,"idx":10,"aOutR":true,"aTimeS":"171400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":19,"idx":11,"aOutR":true,"aTimeS":"171500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":20,"idx":12,"aOutR":true,"aTimeS":"171700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":21,"idx":13,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"type":"N"},{"locX":22,"idx":14,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":24,"idx":15,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":25,"idx":16,"aOutR":true,"aTimeS":"172500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":26,"idx":17,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":16,"idx":18,"aOutR":true,"aTimeS":"172700","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13425165,"y":52481524,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1086924|30|80|2102022","date":"20221002","prodX":25,"dirTxt":"Hermannstr. (S+U), Berlin","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":15,"dProdX":25,"dInR":true,"dTimeS":"171000","dTimeR":"171000","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":15,"dInR":true,"dTimeS":"171000","dTimeR":"171000","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},{"locX":29,"idx":16,"aOutR":true,"aTimeS":"171100","aTZOffset":120,"type":"N"},{"locX":30,"idx":17,"aOutR":true,"aTimeS":"171300","aTZOffset":120,"type":"N"},{"locX":31,"idx":18,"aOutR":true,"aTimeS":"171500","aTZOffset":120,"type":"N"},{"locX":32,"idx":19,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"type":"N"},{"locX":33,"idx":20,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"type":"N"},{"locX":34,"idx":21,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"type":"N"},{"locX":35,"idx":22,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"type":"N"},{"locX":27,"idx":23,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"type":"N"}],"pos":{"x":13389568,"y":52547100,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|306323|4|80|2102022","date":"20221002","prodX":26,"dirTxt":"Berlin-Spandau (S)","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":13,"dProdX":26,"dPlatfS":"4","dInR":true,"dTimeS":"171100","dTimeR":"171100","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":13,"dPlatfS":"4","dInR":true,"dTimeS":"171100","dTimeR":"171100","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":38,"idx":14,"aOutR":true,"aTimeS":"171200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":39,"idx":15,"aOutR":true,"aTimeS":"171400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":40,"idx":16,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":42,"idx":17,"aOutR":true,"aTimeS":"171900","aTZOffset":120,"type":"N"},{"locX":44,"idx":18,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"type":"N"},{"locX":45,"idx":19,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":46,"idx":20,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":48,"idx":21,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":49,"idx":22,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":51,"idx":23,"aOutR":true,"aTimeS":"173200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":52,"idx":24,"aOutR":true,"aTimeS":"173400","aTZOffset":120,"type":"N"},{"locX":53,"idx":25,"aOutR":true,"aTimeS":"173700","aTZOffset":120,"type":"N"},{"locX":54,"idx":26,"aOutR":true,"aTimeS":"173900","aTZOffset":120,"type":"N"},{"locX":55,"idx":27,"aOutR":true,"aTimeS":"174100","aTZOffset":120,"type":"N"},{"locX":56,"idx":28,"aOutR":true,"aTimeS":"174500","aTZOffset":120,"type":"N"},{"locX":36,"idx":29,"aOutR":true,"aTimeS":"174700","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13460241,"y":52491206,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|306331|3|80|2102022","date":"20221002","prodX":27,"dirTxt":"Flughafen BER - Terminal 1-2 (S-Bahn)","dirFlg":"2","status":"P","isCncl":true,"isRchbl":false,"stbStop":{"locX":1,"idx":16,"dProdX":27,"dInR":false,"dTimeS":"171400","dCncl":true,"dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":16,"dInR":false,"dTimeS":"171400","dCncl":true,"dTZOffset":120,"isImp":true,"type":"N"},{"locX":59,"idx":17,"aOutR":false,"aTimeS":"171600","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":61,"idx":18,"aOutR":false,"aTimeS":"171900","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":124,"idx":19,"aOutR":false,"aTimeS":"172200","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":125,"idx":20,"aOutR":false,"aTimeS":"172500","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":126,"idx":21,"aOutR":false,"aTimeS":"172700","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":127,"idx":22,"aOutR":false,"aTimeS":"173300","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":129,"idx":23,"aOutR":false,"aTimeS":"173500","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":130,"idx":24,"aOutR":false,"aTimeS":"173800","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":131,"idx":25,"aOutR":false,"aTimeS":"174200","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":132,"idx":26,"aOutR":false,"aTimeS":"174500","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":133,"idx":27,"aOutR":false,"aTimeS":"174700","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":134,"idx":28,"aOutR":false,"aTimeS":"175200","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"},{"locX":122,"idx":29,"aOutR":false,"aTimeS":"175600","aCncl":true,"aTZOffset":120,"isImp":true,"type":"N"}],"msgL":[{"type":"REM","remX":0,"txtC":{"r":204,"g":0,"b":0,"a":255},"prio":373,"fIdx":-1,"tIdx":-1,"tagL":["RES_JNY_H3"]}],"subscr":"F"},{"jid":"1|302585|0|80|2102022","date":"20221002","prodX":28,"dirTxt":"Berlin Olympiastadion","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":12,"dProdX":28,"dPlatfS":"4","dInR":true,"dTimeS":"171500","dTimeR":"171500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":12,"dPlatfS":"4","dInR":true,"dTimeS":"171500","dTimeR":"171500","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":38,"idx":13,"aOutR":true,"aTimeS":"171700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":39,"idx":14,"aOutR":true,"aTimeS":"171900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":40,"idx":15,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":42,"idx":16,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"type":"N"},{"locX":44,"idx":17,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"type":"N"},{"locX":45,"idx":18,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":46,"idx":19,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":48,"idx":20,"aOutR":true,"aTimeS":"173200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":49,"idx":21,"aOutR":true,"aTimeS":"173400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":51,"idx":22,"aOutR":true,"aTimeS":"173700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":52,"idx":23,"aOutR":true,"aTimeS":"174000","aTZOffset":120,"type":"N"},{"locX":53,"idx":24,"aOutR":true,"aTimeS":"174300","aTZOffset":120,"type":"N"},{"locX":54,"idx":25,"aOutR":true,"aTimeS":"174700","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13524882,"y":52513948,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1085334|63|80|2102022","date":"20221002","prodX":29,"dirTxt":"S+U Wittenau","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":8,"dProdX":29,"dInR":true,"dTimeS":"171500","dTimeR":"171500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":8,"dInR":true,"dTimeS":"171500","dTimeR":"171500","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":17,"idx":9,"aOutR":true,"aTimeS":"171700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":18,"idx":10,"aOutR":true,"aTimeS":"171900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":19,"idx":11,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":20,"idx":12,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":21,"idx":13,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"type":"N"},{"locX":22,"idx":14,"aOutR":true,"aTimeS":"172500","aTZOffset":120,"type":"N"},{"locX":24,"idx":15,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":25,"idx":16,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"type":"N"},{"locX":26,"idx":17,"aOutR":true,"aTimeS":"173100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":16,"idx":18,"aOutR":true,"aTimeS":"173200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":76,"idx":19,"aOutR":true,"aTimeS":"173400","aTZOffset":120,"type":"N"},{"locX":77,"idx":20,"aOutR":true,"aTimeS":"173500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":79,"idx":21,"aOutR":true,"aTimeS":"173700","aTZOffset":120,"type":"N"},{"locX":74,"idx":22,"aOutR":true,"aTimeS":"173800","aTZOffset":120,"isImp":true,"type":"N"}],"subscr":"F"},{"jid":"1|1087305|23|80|2102022","date":"20221002","prodX":30,"dirTxt":"Hermannstr. (S+U), Berlin","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":11,"dProdX":30,"dInR":true,"dTimeS":"171500","dTimeR":"171500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":11,"dInR":true,"dTimeS":"171500","dTimeR":"171500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},{"locX":29,"idx":12,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"type":"N"},{"locX":30,"idx":13,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"type":"N"},{"locX":31,"idx":14,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"type":"N"},{"locX":32,"idx":15,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"type":"N"},{"locX":33,"idx":16,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"type":"N"},{"locX":34,"idx":17,"aOutR":true,"aTimeS":"172500","aTZOffset":120,"type":"N"},{"locX":35,"idx":18,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"type":"N"},{"locX":27,"idx":19,"aOutR":true,"aTimeS":"172700","aTZOffset":120,"type":"N"}],"pos":{"x":13366348,"y":52562427,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1137745|37|80|2102022","date":"20221002","prodX":0,"dirTxt":"Tiergarten, Philharmonie","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":7,"dProdX":0,"dInR":true,"dTimeS":"171500","dTimeR":"171500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":7,"dInR":true,"dTimeS":"171500","dTimeR":"171500","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":3,"idx":8,"aOutR":true,"aTimeS":"171600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":4,"idx":9,"aOutR":true,"aTimeS":"171800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":6,"idx":10,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":7,"idx":11,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":8,"idx":12,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":9,"idx":13,"aOutR":true,"aTimeS":"172500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":10,"idx":14,"aOutR":true,"aTimeS":"172900","aTZOffset":120,"type":"N"},{"locX":11,"idx":15,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":12,"idx":16,"aOutR":true,"aTimeS":"173200","aTZOffset":120,"type":"N"},{"locX":13,"idx":17,"aOutR":true,"aTimeS":"173300","aTZOffset":120,"type":"N"},{"locX":15,"idx":18,"aOutR":true,"aTimeS":"173500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":2,"idx":19,"aOutR":true,"aTimeS":"173800","aTZOffset":120,"isImp":true,"type":"N"}],"subscr":"F"},{"jid":"1|1507932|35|80|2102022","date":"20221002","prodX":20,"dirTxt":"S+U Warschauer Str.","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":11,"dProdX":20,"dInR":true,"dTimeS":"171600","dTimeR":"171600","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":11,"dInR":true,"dTimeS":"171600","dTimeR":"171600","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},{"locX":81,"idx":12,"aOutR":true,"aTimeS":"171700","aTZOffset":120,"type":"N"},{"locX":82,"idx":13,"aOutR":true,"aTimeS":"171900","aTZOffset":120,"type":"N"},{"locX":83,"idx":14,"aOutR":true,"aTimeS":"172000","aTZOffset":120,"type":"N"},{"locX":84,"idx":15,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"type":"N"},{"locX":85,"idx":16,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"type":"N"},{"locX":86,"idx":17,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"type":"N"},{"locX":80,"idx":18,"aOutR":true,"aTimeS":"172700","aTZOffset":120,"type":"N"}],"pos":{"x":13385567,"y":52512995,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|304778|1|80|2102022","date":"20221002","prodX":31,"dirTxt":"Ahrensfelde (S)","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":16,"dProdX":31,"dPlatfS":"3","dInR":true,"dTimeS":"171700","dTimeR":"171700","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":16,"dPlatfS":"3","dInR":true,"dTimeS":"171700","dTimeR":"171700","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":59,"idx":17,"aOutR":true,"aTimeS":"171900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":61,"idx":18,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":62,"idx":19,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":89,"idx":20,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":90,"idx":21,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":92,"idx":22,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":93,"idx":23,"aOutR":true,"aTimeS":"173300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":94,"idx":24,"aOutR":true,"aTimeS":"173600","aTZOffset":120,"type":"N"},{"locX":95,"idx":25,"aOutR":true,"aTimeS":"173800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":96,"idx":26,"aOutR":true,"aTimeS":"173900","aTZOffset":120,"type":"N"},{"locX":97,"idx":27,"aOutR":true,"aTimeS":"174100","aTZOffset":120,"type":"N"},{"locX":87,"idx":28,"aOutR":true,"aTimeS":"174400","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13329762,"y":52506757,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|303931|4|80|2102022","date":"20221002","prodX":32,"dirTxt":"Potsdam Hbf (S)","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":12,"dProdX":32,"dPlatfS":"4","dInR":true,"dTimeS":"171800","dTimeR":"171800","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":12,"dPlatfS":"4","dInR":true,"dTimeS":"171800","dTimeR":"171800","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":38,"idx":13,"aOutR":true,"aTimeS":"171900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":39,"idx":14,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":40,"idx":15,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":42,"idx":16,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"type":"N"},{"locX":44,"idx":17,"aOutR":true,"aTimeS":"172900","aTZOffset":120,"type":"N"},{"locX":45,"idx":18,"aOutR":true,"aTimeS":"173100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":46,"idx":19,"aOutR":true,"aTimeS":"173300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":48,"idx":20,"aOutR":true,"aTimeS":"173500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":49,"idx":21,"aOutR":true,"aTimeS":"173700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":51,"idx":22,"aOutR":true,"aTimeS":"173900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":100,"idx":23,"aOutR":true,"aTimeS":"174200","aTZOffset":120,"type":"N"},{"locX":101,"idx":24,"aOutR":true,"aTimeS":"175000","aTZOffset":120,"type":"N"},{"locX":102,"idx":25,"aOutR":true,"aTimeS":"175200","aTZOffset":120,"type":"N"},{"locX":104,"idx":26,"aOutR":true,"aTimeS":"175700","aTZOffset":120,"type":"N"},{"locX":106,"idx":27,"aOutR":true,"aTimeS":"180000","aTZOffset":120,"type":"N"},{"locX":98,"idx":28,"aOutR":true,"aTimeS":"180200","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13536757,"y":52525733,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|302460|0|80|2102022","date":"20221002","prodX":33,"dirTxt":"Berlin-Mahlsdorf (S)","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":13,"dProdX":33,"dPlatfS":"3","dInR":true,"dTimeS":"171900","dTimeR":"171900","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":13,"dPlatfS":"3","dInR":true,"dTimeS":"171900","dTimeR":"171900","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":59,"idx":14,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":61,"idx":15,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":62,"idx":16,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":89,"idx":17,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":90,"idx":18,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":92,"idx":19,"aOutR":true,"aTimeS":"173300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":108,"idx":20,"aOutR":true,"aTimeS":"173600","aTZOffset":120,"type":"N"},{"locX":109,"idx":21,"aOutR":true,"aTimeS":"173800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":110,"idx":22,"aOutR":true,"aTimeS":"174000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":111,"idx":23,"aOutR":true,"aTimeS":"174400","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13312890,"y":52505480,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|1085848|28|80|2102022","date":"20221002","prodX":34,"dirTxt":"Paracelsus-Bad (U), Berlin","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":8,"dProdX":34,"dInR":true,"dTimeS":"172000","dTimeR":"172000","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":8,"dInR":true,"dTimeS":"172000","dTimeR":"172000","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":17,"idx":9,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":18,"idx":10,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":19,"idx":11,"aOutR":true,"aTimeS":"172500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":20,"idx":12,"aOutR":true,"aTimeS":"172700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":21,"idx":13,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"type":"N"},{"locX":22,"idx":14,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":24,"idx":15,"aOutR":true,"aTimeS":"173300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":25,"idx":16,"aOutR":true,"aTimeS":"173500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":26,"idx":17,"aOutR":true,"aTimeS":"173600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":16,"idx":18,"aOutR":true,"aTimeS":"173700","aTZOffset":120,"isImp":true,"type":"N"}],"subscr":"F"},{"jid":"1|1086924|31|80|2102022","date":"20221002","prodX":35,"dirTxt":"Hermannstr. (S+U), Berlin","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":0,"idx":15,"dProdX":35,"dInR":true,"dTimeS":"172000","dTimeR":"172000","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":0,"idx":15,"dInR":true,"dTimeS":"172000","dTimeR":"172000","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},{"locX":29,"idx":16,"aOutR":true,"aTimeS":"172100","aTZOffset":120,"type":"N"},{"locX":30,"idx":17,"aOutR":true,"aTimeS":"172300","aTZOffset":120,"type":"N"},{"locX":31,"idx":18,"aOutR":true,"aTimeS":"172500","aTZOffset":120,"type":"N"},{"locX":32,"idx":19,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"type":"N"},{"locX":33,"idx":20,"aOutR":true,"aTimeS":"172800","aTZOffset":120,"type":"N"},{"locX":34,"idx":21,"aOutR":true,"aTimeS":"173000","aTZOffset":120,"type":"N"},{"locX":35,"idx":22,"aOutR":true,"aTimeS":"173100","aTZOffset":120,"type":"N"},{"locX":27,"idx":23,"aOutR":true,"aTimeS":"173200","aTZOffset":120,"type":"N"}],"pos":{"x":13336432,"y":52576855,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|296882|4|80|2102022","date":"20221002","prodX":36,"dirTxt":"Berlin-Spandau (S)","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":13,"dProdX":36,"dPlatfS":"4","dInR":true,"dTimeS":"172100","dTimeR":"172100","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":13,"dPlatfS":"4","dInR":true,"dTimeS":"172100","dTimeR":"172100","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":38,"idx":14,"aOutR":true,"aTimeS":"172200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":39,"idx":15,"aOutR":true,"aTimeS":"172400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":40,"idx":16,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":42,"idx":17,"aOutR":true,"aTimeS":"172900","aTZOffset":120,"type":"N"},{"locX":44,"idx":18,"aOutR":true,"aTimeS":"173200","aTZOffset":120,"type":"N"},{"locX":45,"idx":19,"aOutR":true,"aTimeS":"173400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":46,"idx":20,"aOutR":true,"aTimeS":"173600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":48,"idx":21,"aOutR":true,"aTimeS":"173800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":49,"idx":22,"aOutR":true,"aTimeS":"174000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":51,"idx":23,"aOutR":true,"aTimeS":"174200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":52,"idx":24,"aOutR":true,"aTimeS":"174400","aTZOffset":120,"type":"N"},{"locX":53,"idx":25,"aOutR":true,"aTimeS":"174700","aTZOffset":120,"type":"N"},{"locX":54,"idx":26,"aOutR":true,"aTimeS":"174900","aTZOffset":120,"type":"N"},{"locX":55,"idx":27,"aOutR":true,"aTimeS":"175100","aTZOffset":120,"type":"N"},{"locX":56,"idx":28,"aOutR":true,"aTimeS":"175500","aTZOffset":120,"type":"N"},{"locX":36,"idx":29,"aOutR":true,"aTimeS":"175700","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13575231,"y":52459546,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|296884|3|80|2102022","date":"20221002","prodX":37,"dirTxt":"Erkner (S)","dirFlg":"2","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":16,"dProdX":37,"dPlatfS":"3","dInR":true,"dTimeS":"172400","dTimeR":"172400","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":16,"dPlatfS":"3","dInR":true,"dTimeS":"172400","dTimeR":"172400","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":59,"idx":17,"aOutR":true,"aTimeS":"172600","aTZOffset":120,"isImp":true,"type":"N"},{"locX":61,"idx":18,"aOutR":true,"aTimeS":"172900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":62,"idx":19,"aOutR":true,"aTimeS":"173100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":64,"idx":20,"aOutR":true,"aTimeS":"173300","aTZOffset":120,"isImp":true,"type":"N"},{"locX":65,"idx":21,"aOutR":true,"aTimeS":"173500","aTZOffset":120,"isImp":true,"type":"N"},{"locX":66,"idx":22,"aOutR":true,"aTimeS":"173800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":68,"idx":23,"aOutR":true,"aTimeS":"174100","aTZOffset":120,"type":"N"},{"locX":69,"idx":24,"aOutR":true,"aTimeS":"174400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":70,"idx":25,"aOutR":true,"aTimeS":"174600","aTZOffset":120,"type":"N"},{"locX":71,"idx":26,"aOutR":true,"aTimeS":"174900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":72,"idx":27,"aOutR":true,"aTimeS":"175300","aTZOffset":120,"type":"N"},{"locX":73,"idx":28,"aOutR":true,"aTimeS":"175700","aTZOffset":120,"type":"N"},{"locX":57,"idx":29,"aOutR":true,"aTimeS":"180000","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13279674,"y":52497291,"layerX":0,"crdSysX":0},"subscr":"F"},{"jid":"1|301830|0|80|2102022","date":"20221002","prodX":38,"dirTxt":"Berlin Olympiastadion","dirFlg":"1","status":"P","isRchbl":true,"stbStop":{"locX":1,"idx":19,"dProdX":38,"dPlatfS":"4","dInR":true,"dTimeS":"172500","dTimeR":"172500","dProgType":"PROGNOSED","dTZOffset":120,"type":"N"},"stopL":[{"locX":1,"idx":19,"dPlatfS":"4","dInR":true,"dTimeS":"172500","dTimeR":"172500","dProgType":"PROGNOSED","dTZOffset":120,"isImp":true,"type":"N"},{"locX":38,"idx":20,"aOutR":true,"aTimeS":"172700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":39,"idx":21,"aOutR":true,"aTimeS":"172900","aTZOffset":120,"isImp":true,"type":"N"},{"locX":40,"idx":22,"aOutR":true,"aTimeS":"173100","aTZOffset":120,"isImp":true,"type":"N"},{"locX":42,"idx":23,"aOutR":true,"aTimeS":"173400","aTZOffset":120,"type":"N"},{"locX":44,"idx":24,"aOutR":true,"aTimeS":"173600","aTZOffset":120,"type":"N"},{"locX":45,"idx":25,"aOutR":true,"aTimeS":"173800","aTZOffset":120,"isImp":true,"type":"N"},{"locX":46,"idx":26,"aOutR":true,"aTimeS":"174000","aTZOffset":120,"isImp":true,"type":"N"},{"locX":48,"idx":27,"aOutR":true,"aTimeS":"174200","aTZOffset":120,"isImp":true,"type":"N"},{"locX":49,"idx":28,"aOutR":true,"aTimeS":"174400","aTZOffset":120,"isImp":true,"type":"N"},{"locX":51,"idx":29,"aOutR":true,"aTimeS":"174700","aTZOffset":120,"isImp":true,"type":"N"},{"locX":52,"idx":30,"aOutR":true,"aTimeS":"175000","aTZOffset":120,"type":"N"},{"locX":53,"idx":31,"aOutR":true,"aTimeS":"175300","aTZOffset":120,"type":"N"},{"locX":54,"idx":32,"aOutR":true,"aTimeS":"175700","aTZOffset":120,"isImp":true,"type":"N"}],"pos":{"x":13611907,"y":52512249,"layerX":0,"crdSysX":0},"subscr":"F"}],"fpB":"20211212","fpE":"20221210","planrtTS":"1664722750","sD":"20221002","sT":"170028"}}]} diff --git a/t/in/DB.EC392.journey.json b/t/in/DB.EC392.journey.json new file mode 100644 index 0000000..0f65c7a --- /dev/null +++ b/t/in/DB.EC392.journey.json @@ -0,0 +1 @@ +{"svcResL":[{"meth":"JourneyDetails","res":{"journey":{"dTrnCmpSX":{"tcocX":[2,3]},"sDaysL":[{"sDaysB":"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000FFF7FE000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","sDaysR":"fährt 17. Dez 2023 bis 1. Jan 2024; nicht 22. Dez 2023 ","tLocX":3,"fLocX":0}],"dirTxt":"Koebenhavn H","stopL":[{"dPlatfS":"12C-F","locX":0,"dProgType":"PROGNOSED","dDirFlg":"2","type":"N","dTimeR":"150300","dTimeS":"145300","dPlatfCh":true,"dTZOffset":60,"dTrnCmpSX":{"tcocX":[0,1]},"dInR":true,"dDirTxt":"Koebenhavn H","dProdX":0,"dPlatfR":"12A-B","msgL":[{"type":"REM","fIdx":-1,"tagL":["RES_LOC_H3"],"remX":0,"tIdx":-1,"prio":478,"txtC":{"r":204,"a":255,"g":0,"b":0}},{"prio":478,"tIdx":-1,"type":"REM","remX":1,"tagL":["RES_LOC_H3"],"fIdx":-1},{"type":"REM","tagL":["RES_LOC_H3"],"remX":5}],"idx":0},{"dProdX":0,"msgL":[{"tagL":["RES_LOC_H3"],"remX":6,"type":"REM"}],"idx":1,"dDirTxt":"København H","dInR":true,"aProdX":0,"type":"N","dTimeR":"163200","aProgType":"PROGNOSED","dTimeS":"162800","aPlatfS":"1","dTZOffset":60,"locX":1,"dPlatfS":"1","aTZOffset":60,"aTimeS":"162600","dProgType":"PROGNOSED","aTimeR":"163000","aOutR":true},{"dTimeR":"170100","aProgType":"PROGNOSED","type":"N","dTZOffset":60,"dTimeS":"165900","locX":2,"aTimeS":"165300","dPlatfS":"2","aTZOffset":60,"dDirFlg":"1","dProgType":"PROGNOSED","aTimeR":"165700","aOutR":true,"dProdX":1,"idx":3,"dInR":true,"aProdX":0},{"dTZOffset":60,"aPlatfS":"2","dTimeS":"174200","aProgType":"PROGNOSED","dTimeR":"174200","type":"N","aTimeR":"174200","dProgType":"PROGNOSED","aOutR":true,"locX":4,"aTimeS":"174000","dPlatfS":"2","aTZOffset":60,"idx":4,"dProdX":1,"aProdX":1,"dInR":true},{"type":"N","aProgType":"PROGNOSED","dTimeR":"182300","dTimeS":"182300","aPlatfS":"3","dTZOffset":60,"aTZOffset":60,"dPlatfS":"3","aTimeS":"182000","locX":5,"aOutR":true,"aTimeR":"182000","dProgType":"PROGNOSED","dProdX":1,"idx":5,"dInR":true,"aProdX":1},{"dProdX":1,"idx":6,"dInR":true,"aProdX":1,"dTimeR":"190700","aProgType":"PROGNOSED","type":"N","aPlatfS":"2","dTZOffset":60,"dTimeS":"190700","aTimeS":"190200","dPlatfS":"2","aTZOffset":60,"locX":6,"aOutR":true,"aTimeR":"190200","dProgType":"PROGNOSED"},{"idx":7,"aPlatfS":"5","type":"N","aProgType":"PROGNOSED","aTimeR":"193600","aOutR":true,"aProdX":1,"locX":3,"aTZOffset":60,"aTimeS":"193600"}],"isRchbl":true,"subscr":"F","msgL":[{"tIdx":7,"tLocX":3,"remX":2,"tagL":["RES_JNY_DTL"],"fIdx":1,"type":"REM","fLocX":1},{"type":"REM","remX":3,"tagL":["RES_JNY_DTL"],"fIdx":1,"fLocX":1,"tIdx":7,"tLocX":3},{"tIdx":1,"tLocX":1,"remX":4,"tagL":["RES_JNY_DTL"],"fIdx":0,"type":"REM","fLocX":0},{"tagL":["SUM_JNY_H3"],"remX":7,"type":"REM"}],"status":"P","jid":"1|197782|0|81|17122023","date":"20231217","dirFlg":"2","prodX":0},"fpB":"20221211","planrtTS":"1702821591","common":{"icoL":[{"res":"EC"},{"txt":"DB Fernverkehr AG","res":"D"},{"res":"ICE"},{"txt":"Dänische Staatsbahnen","res":"ZUG"},{"res":"ICL"},{"res":"Empty"},{"res":"attr_info"}],"locL":[{"type":"S","extId":"8002549","state":"F","lid":"A=1@O=Hamburg Hbf@X=10006909@Y=53552733@U=81@L=8002549@","pCls":191,"name":"Hamburg Hbf","icoX":2,"crd":{"layerX":0,"x":10006360,"z":0,"crdSysX":0,"y":53553533,"type":"WGS84"}},{"icoX":0,"name":"Schleswig","crd":{"layerX":0,"crdSysX":0,"x":9537438,"z":0,"y":54499881,"type":"WGS84"},"lid":"A=1@O=Schleswig@X=9538139@Y=54499459@U=81@L=8005362@","state":"F","extId":"8005362","type":"S","pCls":42},{"name":"Padborg st","icoX":0,"crd":{"layerX":0,"x":9356225,"z":0,"crdSysX":0,"y":54829057,"type":"WGS84"},"extId":"8601899","lid":"A=1@O=Padborg st@X=9358804@Y=54824248@U=81@L=8601899@","state":"F","type":"S","pCls":10},{"icoX":4,"name":"Koebenhavn H","crd":{"y":55672722,"type":"WGS84","crdSysX":0,"x":12564618,"layerX":0},"extId":"8601309","lid":"A=1@O=Koebenhavn H@X=12564618@Y=55672722@U=81@L=8601309@","state":"F","type":"S","pCls":27},{"pCls":15,"lid":"A=1@O=Kolding st@X=9481525@Y=55490843@U=81@L=8601318@","extId":"8601318","state":"F","type":"S","crd":{"y":55490843,"type":"WGS84","x":9481525,"crdSysX":0,"layerX":0},"icoX":4,"name":"Kolding st"},{"icoX":4,"name":"Odense st","crd":{"y":55401778,"type":"WGS84","layerX":0,"x":10386002,"crdSysX":0},"type":"S","lid":"A=1@O=Odense st@X=10386002@Y=55401778@U=81@L=8601770@","extId":"8601770","state":"F","pCls":271},{"crd":{"y":55438040,"type":"WGS84","layerX":0,"crdSysX":0,"x":11786153},"icoX":4,"name":"Ringsted st","pCls":10,"type":"S","lid":"A=1@O=Ringsted st@X=11786153@Y=55438040@U=81@L=8601979@","extId":"8601979","state":"F"}],"crdSysL":[{"index":0,"id":"standard","dim":3,"type":"WGS84"}],"layerL":[{"annoCnt":0,"name":"standard","id":"standard","index":0}],"opL":[{"name":"DB Fernverkehr AG","icoX":1},{"name":"Dänische Staatsbahnen","icoX":3}],"tcocL":[{"r":3,"c":"FIRST"},{"r":3,"c":"SECOND"},{"c":"FIRST","r":3},{"r":3,"c":"SECOND"}],"remL":[{"icoX":5,"type":"D","code":"","txtN":"Verspätung aus vorheriger Fahrt"},{"type":"G","icoX":5,"txtN":"Gleiswechsel","code":""},{"type":"A","icoX":6,"prio":260,"txtN":"keine Fahrradbeförderung möglich (Schleswig - Koebenhavn H)","code":"NF"},{"prio":320,"code":"RE","txtN":"Bitte reservieren (Schleswig - Koebenhavn H)","type":"A","icoX":6},{"code":"KG","txtN":"Ohne gastronomisches Angebot (Hamburg Hbf - Schleswig)","prio":640,"type":"A","icoX":6},{"icoX":6,"type":"A","code":"text.journeystop.product.or.direction.changes.stop.message","txtN":"Verkehrt ab hier als EC 392 in Richtung Koebenhavn H"},{"type":"A","icoX":6,"txtN":"Verkehrt ab hier als EC 392 in Richtung København H","code":"text.journeystop.product.or.direction.changes.stop.message"},{"icoX":6,"type":"A","code":"text.journeystop.product.or.direction.changes.journey.message","txtN":"Verkehrt ab Schleswig als EC 392 in Richtung København H"}],"polyL":[],"prodL":[{"name":"EC 392","icoX":0,"number":"392","oprX":0,"cls":2,"prodCtx":{"name":"EC 392","catOut":"EC","catIn":"EC","catCode":"1","catOutS":"EC","matchId":"75","catOutL":"Eurocity","admin":"80____","num":"392"}},{"cls":2,"prodCtx":{"num":"392","admin":"86____","catOutL":"Eurocity","matchId":"392","catOutS":"EC","catCode":"1","name":"EC 392","catIn":"EC","catOut":"EC"},"name":"EC 392","icoX":0,"number":"392","oprX":1}]},"fpE":"20241214"},"err":"OK"}],"ver":"1.15","ext":"DB.R21.12.a","id":"hb6i5jekkqud9w4s","cInfo":{"code":"OK"},"lang":"deu"} diff --git a/t/in/DB.ICE23.journey.json b/t/in/DB.ICE23.journey.json new file mode 100644 index 0000000..5264769 --- /dev/null +++ b/t/in/DB.ICE23.journey.json @@ -0,0 +1 @@ +{"id":"2d4m7jaum2scww8g","lang":"deu","ver":"1.15","cInfo":{"code":"OK"},"svcResL":[{"res":{"planrtTS":"1702800592","fpE":"20241214","fpB":"20221211","common":{"opL":[{"name":"DB Fernverkehr AG","icoX":1},{"name":"Österreichische Bundesbahnen","icoX":3}],"polyL":[],"prodL":[{"number":"23","name":"ICE 23","cls":1,"prodCtx":{"admin":"80____","catCode":"0","catOutL":"Intercity-Express","matchId":"91","num":"23","name":"ICE 23","catIn":"ICE","catOut":"ICE","catOutS":"ICE"},"oprX":0,"icoX":0},{"name":"ICE 23","icoX":0,"oprX":1,"prodCtx":{"catOutS":"ICE","catOut":"ICE","catIn":"ICE","name":"ICE 23","num":"23","matchId":"23","catOutL":"Intercity-Express","catCode":"0","admin":"81____"},"cls":1,"number":"23"}],"layerL":[{"annoCnt":0,"id":"standard","index":0,"name":"standard"}],"tcocL":[{"c":"FIRST","r":1},{"r":1,"c":"SECOND"},{"r":1,"c":"FIRST"},{"c":"SECOND","r":1},{"r":1,"c":"FIRST"},{"c":"SECOND","r":1},{"c":"FIRST","r":1},{"r":1,"c":"SECOND"},{"r":1,"c":"FIRST"},{"r":1,"c":"SECOND"},{"c":"FIRST","r":1},{"c":"SECOND","r":1},{"r":1,"c":"FIRST"},{"c":"SECOND","r":1},{"r":1,"c":"FIRST"},{"c":"SECOND","r":1},{"c":"FIRST","r":1},{"r":2,"c":"SECOND"},{"c":"FIRST","r":1},{"c":"SECOND","r":2},{"c":"FIRST","r":1},{"c":"SECOND","r":2},{"r":2,"c":"FIRST"},{"r":2,"c":"SECOND"},{"r":3,"c":"FIRST"},{"r":2,"c":"SECOND"},{"r":2,"c":"FIRST"},{"r":2,"c":"SECOND"},{"r":2,"c":"FIRST"},{"r":2,"c":"SECOND"},{"c":"FIRST","r":3},{"r":2,"c":"SECOND"}],"locL":[{"type":"S","name":"Dortmund Hbf","pCls":191,"extId":"8000080","crd":{"type":"WGS84","y":51517872,"x":7459276,"crdSysX":0,"layerX":0,"z":0},"icoX":2,"lid":"A=1@O=Dortmund Hbf@X=7459294@Y=51517899@U=81@L=8000080@","state":"F"},{"extId":"8000298","icoX":0,"crd":{"x":13450775,"y":48573778,"type":"WGS84","z":0,"layerX":0,"crdSysX":0},"pCls":555,"name":"Passau Hbf","type":"S","state":"F","lid":"A=1@O=Passau Hbf@X=13450775@Y=48573635@U=81@L=8000298@"},{"extId":"8103000","crd":{"crdSysX":0,"layerX":0,"type":"WGS84","y":48185103,"x":16377114},"icoX":0,"pCls":415,"name":"Wien Hbf","type":"S","state":"F","lid":"A=1@O=Wien Hbf@X=16377114@Y=48185103@U=81@L=8103000@"},{"state":"F","lid":"A=1@O=Bochum Hbf@X=7223273@Y=51478607@U=81@L=8000041@","icoX":0,"crd":{"layerX":0,"crdSysX":0,"z":0,"type":"WGS84","x":7223264,"y":51478490},"extId":"8000041","pCls":447,"name":"Bochum Hbf","type":"S"},{"name":"Essen Hbf","type":"S","icoX":2,"crd":{"type":"WGS84","x":7013860,"y":51451378,"layerX":0,"crdSysX":0,"z":0},"extId":"8000098","pCls":447,"state":"F","lid":"A=1@O=Essen Hbf@X=7014795@Y=51451351@U=81@L=8000098@"},{"state":"F","lid":"A=1@O=Duisburg Hbf@X=6775907@Y=51429786@U=81@L=8000086@","name":"Duisburg Hbf","type":"S","icoX":2,"crd":{"type":"WGS84","y":51429615,"x":6776060,"crdSysX":0,"layerX":0,"z":0},"extId":"8000086","pCls":447},{"state":"F","lid":"A=1@O=Düsseldorf Hbf@X=6794317@Y=51219960@U=81@L=8000085@","name":"Düsseldorf Hbf","type":"S","extId":"8000085","icoX":2,"crd":{"layerX":0,"crdSysX":0,"z":0,"type":"WGS84","x":6794011,"y":51219708},"pCls":447},{"state":"F","lid":"A=1@O=Köln Hbf@X=6958730@Y=50943029@U=81@L=8000207@","name":"Köln Hbf","type":"S","extId":"8000207","crd":{"crdSysX":0,"layerX":0,"z":0,"type":"WGS84","y":50942823,"x":6959197},"icoX":2,"pCls":319},{"extId":"8000044","crd":{"y":50731963,"x":7096678,"type":"WGS84","z":0,"crdSysX":0,"layerX":0},"icoX":0,"pCls":319,"name":"Bonn Hbf","type":"S","state":"F","lid":"A=1@O=Bonn Hbf@X=7097136@Y=50732008@U=81@L=8000044@"},{"lid":"A=1@O=Koblenz Hbf@X=7588343@Y=50350928@U=81@L=8000206@","state":"F","type":"S","name":"Koblenz Hbf","pCls":559,"extId":"8000206","icoX":0,"crd":{"layerX":0,"crdSysX":0,"z":0,"type":"WGS84","x":7588343,"y":50350775}},{"name":"Mainz Hbf","type":"S","icoX":0,"crd":{"layerX":0,"crdSysX":0,"z":0,"type":"WGS84","x":8258453,"y":50001239},"extId":"8000240","pCls":319,"state":"F","lid":"A=1@O=Mainz Hbf@X=8258723@Y=50001113@U=81@L=8000240@"},{"lid":"A=1@O=Frankfurt(M) Flughafen Fernbf@X=8570181@Y=50053169@U=81@L=8070003@","state":"F","pCls":31,"icoX":0,"crd":{"type":"WGS84","y":50052926,"x":8569776,"crdSysX":0,"layerX":0,"z":0},"extId":"8070003","type":"S","name":"Frankfurt(M) Flughafen Fernbf"},{"state":"F","lid":"A=1@O=Frankfurt(Main)Hbf@X=8663785@Y=50107149@U=81@L=8000105@","icoX":4,"crd":{"z":0,"layerX":0,"crdSysX":0,"x":8663003,"y":50106817,"type":"WGS84"},"extId":"8000105","pCls":447,"name":"Frankfurt(Main)Hbf","type":"S"},{"state":"F","lid":"A=1@O=Hanau Hbf@X=8929003@Y=50120957@U=81@L=8000150@","extId":"8000150","crd":{"layerX":0,"crdSysX":0,"z":0,"type":"WGS84","x":8929210,"y":50120903},"icoX":0,"pCls":575,"name":"Hanau Hbf","type":"S"},{"lid":"A=1@O=Würzburg Hbf@X=9935777@Y=49801795@U=81@L=8000260@","state":"F","type":"S","name":"Würzburg Hbf","pCls":299,"crd":{"type":"WGS84","y":49802163,"x":9935930,"crdSysX":0,"layerX":0,"z":0},"icoX":0,"extId":"8000260"},{"state":"F","lid":"A=1@O=Nürnberg Hbf@X=11082989@Y=49445615@U=81@L=8000284@","name":"Nürnberg Hbf","type":"S","crd":{"z":0,"crdSysX":0,"layerX":0,"y":49445435,"x":11082270,"type":"WGS84"},"icoX":0,"extId":"8000284","pCls":447},{"icoX":0,"crd":{"type":"WGS84","x":12099669,"y":49011751,"layerX":0,"crdSysX":0,"z":0},"extId":"8000309","pCls":555,"name":"Regensburg Hbf","type":"S","state":"F","lid":"A=1@O=Regensburg Hbf@X=12099615@Y=49011670@U=81@L=8000309@"},{"lid":"A=1@O=Plattling@X=12863941@Y=48779622@U=81@L=8000301@","state":"F","type":"S","name":"Plattling","pCls":555,"extId":"8000301","crd":{"z":0,"crdSysX":0,"layerX":0,"y":48779604,"x":12863959,"type":"WGS84"},"icoX":0},{"state":"F","lid":"A=1@O=Linz Hbf@X=14292129@Y=48290178@U=81@L=8100013@","extId":"8100013","crd":{"layerX":0,"crdSysX":0,"type":"WGS84","x":14292129,"y":48290178},"icoX":0,"pCls":287,"name":"Linz Hbf","type":"S"},{"state":"F","lid":"A=1@O=St.Pölten Hbf@X=15624672@Y=48208304@U=81@L=8100008@","name":"St.Pölten Hbf","type":"S","icoX":0,"crd":{"type":"WGS84","y":48208304,"x":15624672,"crdSysX":0,"layerX":0},"extId":"8100008","pCls":31},{"lid":"A=1@O=Wien Meidling@X=16333085@Y=48174451@U=81@L=8100514@","state":"F","type":"S","name":"Wien Meidling","pCls":159,"crd":{"type":"WGS84","y":48174451,"x":16333085,"crdSysX":0,"layerX":0},"icoX":0,"extId":"8100514"}],"crdSysL":[{"dim":3,"type":"WGS84","id":"standard","index":0}],"remL":[{"prio":200,"icoX":5,"code":"CK","type":"A","txtN":"Sicher & kontaktlos ohne Ticketkontrolle reisen mit Komfort Check-in (Dortmund Hbf - Passau Hbf)"},{"code":"FR","icoX":6,"prio":260,"type":"A","txtN":"Fahrradmitnahme reservierungspflichtig"},{"icoX":7,"code":"FB","prio":260,"txtN":"Fahrradmitnahme begrenzt möglich (Dortmund Hbf - Passau Hbf)","type":"A"},{"prio":260,"code":"FB","icoX":7,"txtN":"Fahrradmitnahme begrenzt möglich (Passau Hbf - Wien Hbf)","type":"A"},{"code":"BR","icoX":8,"prio":450,"type":"A","txtN":"Bordrestaurant (Passau Hbf - Wien Hbf)"},{"code":"RO","icoX":9,"prio":560,"type":"A","txtN":"Rollstuhlstellplatz (Passau Hbf - Wien Hbf)"},{"prio":560,"code":"OA","icoX":5,"txtN":"Rollstuhlstellplatz - Voranmeldung unter +43 5 1717 (Passau Hbf - Wien Hbf)","type":"A"},{"prio":560,"code":"OC","icoX":5,"txtN":"rollstuhltaugliches WC (Passau Hbf - Wien Hbf)","type":"A"},{"type":"A","txtN":"Ruhezone (Passau Hbf - Wien Hbf)","code":"HD","icoX":5,"prio":605},{"prio":610,"icoX":10,"code":"KK","type":"A","txtN":"Kleinkindabteil (Passau Hbf - Wien Hbf)"},{"prio":710,"code":"WV","icoX":5,"txtN":"WLAN verfügbar (Passau Hbf - Wien Hbf)","type":"A"},{"txtN":"Wagen 37 verkehrt abweichend als 1./2. Klasse-Sitzwagen statt Bordbistro mit Kleinkindabteil. Das Kleinkindabteil befindet sich in Wagen 36. Der Zug verkehrt mit zusätzlichen Sitzplätzen.","type":"M","txtS":"Würzburg Hbf->Passau Hbf: Information. ","code":"","icoX":11}],"icoL":[{"res":"ICE"},{"txt":"DB Fernverkehr AG","res":"D"},{"res":"EST"},{"res":"D","txt":"Österreichische Bundesbahnen"},{"res":"ECE"},{"res":"attr_info"},{"res":"attr_bike_r"},{"res":"attr_bike"},{"res":"attr_resto"},{"res":"attr_wchair"},{"res":"attr_baby"},{"res":"HimInfo"}]},"journey":{"sDaysL":[{"fLocX":0,"tLocX":2,"sDaysB":"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FE4C000000000007FFFFFFFF0000FFFFFFFFFFFFFFFFFFFFFFFFE00000000000000000000000000000000000000","sDaysR":"fährt 17. Dez 2023 bis 15. Jul 2024; nicht 25., 26., 28., 29. Dez 2023, 1. Jan bis 16. Feb 2024, 23. Mär bis 7. Apr 2024 "}],"date":"20231217","subscr":"F","dirFlg":"1","dirTxt":"Wien Hbf","msgL":[{"type":"REM","fLocX":0,"tIdx":16,"fIdx":0,"tLocX":1,"remX":0,"tagL":["RES_JNY_DTL"]},{"tIdx":20,"fLocX":0,"type":"REM","tagL":["RES_JNY_DTL"],"remX":1,"tLocX":2,"fIdx":0},{"remX":2,"tagL":["RES_JNY_DTL"],"tLocX":1,"fIdx":0,"tIdx":16,"fLocX":0,"type":"REM"},{"tagL":["RES_JNY_DTL"],"remX":3,"tLocX":2,"fIdx":16,"tIdx":20,"fLocX":1,"type":"REM"},{"tagL":["RES_JNY_DTL"],"remX":4,"tLocX":2,"fIdx":16,"tIdx":20,"fLocX":1,"type":"REM"},{"type":"REM","fLocX":1,"tIdx":20,"fIdx":16,"tLocX":2,"remX":5,"tagL":["RES_JNY_DTL"]},{"fIdx":16,"tLocX":2,"tagL":["RES_JNY_DTL"],"remX":6,"type":"REM","fLocX":1,"tIdx":20},{"tLocX":2,"fIdx":16,"remX":7,"tagL":["RES_JNY_DTL"],"type":"REM","tIdx":20,"fLocX":1},{"type":"REM","tIdx":20,"fLocX":1,"tLocX":2,"fIdx":16,"tagL":["RES_JNY_DTL"],"remX":8},{"fIdx":16,"tLocX":2,"remX":9,"tagL":["RES_JNY_DTL"],"type":"REM","fLocX":1,"tIdx":20},{"fIdx":16,"tLocX":2,"remX":10,"tagL":["RES_JNY_DTL"],"type":"REM","fLocX":1,"tIdx":20},{"tIdx":-1,"type":"REM","tagL":["RES_JNY_H2"],"remX":11,"fIdx":-1,"prio":120}],"pos":{"layerX":0,"crdSysX":0,"x":9540458,"y":50025842},"stopL":[{"dTrnCmpSX":{"tcocX":[0,1]},"dDirTxt":"Wien Hbf","dProgType":"REPORTED","type":"N","dTimeS":"043400","idx":0,"dDirFlg":"1","locX":0,"dInR":true,"dProdX":0,"dPlatfS":"16","dTZOffset":60},{"type":"N","dProgType":"REPORTED","dTrnCmpSX":{"tcocX":[2,3]},"idx":1,"aOutR":true,"aTZOffset":60,"aPlatfS":"3","aProdX":0,"dTimeS":"044800","dInR":true,"aProgType":"REPORTED","locX":3,"dTZOffset":60,"dPlatfS":"3","dProdX":0,"aTimeS":"044700"},{"dInR":true,"aProgType":"REPORTED","locX":4,"dTZOffset":60,"dPlatfS":"2","dProdX":0,"aTimeS":"045900","type":"N","dProgType":"REPORTED","dTrnCmpSX":{"tcocX":[4,5]},"aTZOffset":60,"aOutR":true,"idx":2,"aPlatfS":"2","aProdX":0,"dTimeS":"050100"},{"idx":3,"aTZOffset":60,"aOutR":true,"aPlatfS":"3","dTimeS":"051400","aProdX":0,"type":"N","dProgType":"REPORTED","dTrnCmpSX":{"tcocX":[6,7]},"dTZOffset":60,"dPlatfS":"3","aTimeS":"051200","dProdX":0,"aProgType":"REPORTED","dInR":true,"locX":5},{"dProgType":"REPORTED","type":"N","dTrnCmpSX":{"tcocX":[8,9]},"idx":4,"aOutR":true,"aTZOffset":60,"aProdX":0,"dTimeS":"052700","aPlatfS":"16","dInR":true,"aProgType":"REPORTED","locX":6,"dTZOffset":60,"dProdX":0,"aTimeS":"052500","dPlatfS":"16"},{"aProgType":"REPORTED","dInR":true,"locX":7,"dTZOffset":60,"aTimeS":"055000","dProdX":0,"dPlatfS":"7","dProgType":"REPORTED","type":"N","dTrnCmpSX":{"tcocX":[10,11]},"aOutR":true,"idx":5,"aTZOffset":60,"dTimeS":"055300","aProdX":0,"aPlatfS":"7"},{"dTrnCmpSX":{"tcocX":[12,13]},"type":"N","dProgType":"REPORTED","aPlatfS":"3","dTimeS":"061400","aProdX":0,"idx":6,"aTZOffset":60,"aOutR":true,"locX":8,"aProgType":"REPORTED","dInR":true,"dPlatfS":"3","aTimeS":"061200","dProdX":0,"dTZOffset":60},{"dTZOffset":60,"dPlatfS":"4","dProdX":0,"aTimeS":"064600","dInR":true,"aProgType":"REPORTED","locX":9,"idx":7,"aOutR":true,"aTZOffset":60,"aPlatfS":"4","aProdX":0,"dTimeS":"064800","type":"N","dProgType":"REPORTED","dTrnCmpSX":{"tcocX":[14,15]}},{"locX":10,"dInR":true,"aProgType":"REPORTED","dProdX":0,"aTimeS":"073800","dPlatfS":"5a/b","dTZOffset":60,"dTrnCmpSX":{"tcocX":[16,17]},"dProgType":"REPORTED","type":"N","aProdX":0,"dTimeS":"074000","aPlatfS":"5a/b","aOutR":true,"aTZOffset":60,"idx":8},{"dInR":true,"aProgType":"REPORTED","locX":11,"dTZOffset":60,"dPlatfS":"Fern 5","dProdX":0,"aTimeS":"075900","type":"N","dProgType":"REPORTED","idx":9,"aOutR":true,"aTZOffset":60,"aPlatfS":"Fern 5","aProdX":0,"dTimeS":"080200"},{"aPlatfS":"6","aProdX":0,"dTimeS":"082100","aOutR":true,"aTZOffset":60,"idx":10,"dTrnCmpSX":{"tcocX":[18,19]},"type":"N","dProgType":"REPORTED","dPlatfS":"6","dProdX":0,"aTimeS":"081400","dTZOffset":60,"locX":12,"dInR":true,"aProgType":"REPORTED"},{"dProgType":"REPORTED","type":"N","dTrnCmpSX":{"tcocX":[20,21]},"idx":11,"aOutR":true,"aTZOffset":60,"dTimeS":"083800","aProdX":0,"aPlatfS":"103","aProgType":"REPORTED","dInR":true,"locX":13,"dTZOffset":60,"aTimeS":"083700","dProdX":0,"dPlatfS":"103"},{"type":"N","dProgType":"PROGNOSED","aTimeR":"093300","dTrnCmpSX":{"tcocX":[22,23]},"aOutR":true,"idx":12,"aTZOffset":60,"aPlatfS":"5","aProdX":0,"dTimeS":"093400","dInR":true,"aProgType":"PROGNOSED","locX":14,"dTimeR":"093600","dTZOffset":60,"dPlatfS":"5","dProdX":0,"aTimeS":"093200"},{"type":"N","dProgType":"PROGNOSED","dTrnCmpSX":{"tcocX":[24,25]},"aTimeR":"102900","aOutR":true,"idx":13,"aTZOffset":60,"aPlatfS":"9","aProdX":0,"dTimeS":"103100","dInR":true,"aProgType":"PROGNOSED","locX":15,"dTimeR":"103300","dTZOffset":60,"dPlatfS":"9","dProdX":0,"aTimeS":"102700"},{"dInR":true,"aProgType":"PROGNOSED","locX":16,"dTimeR":"112800","dTZOffset":60,"dPlatfS":"9","dProdX":0,"aTimeS":"112300","type":"N","dProgType":"PROGNOSED","aTimeR":"112600","dTrnCmpSX":{"tcocX":[26,27]},"aOutR":true,"idx":14,"aTZOffset":60,"aPlatfS":"9","aProdX":0,"dTimeS":"112500"},{"dTimeR":"115900","locX":17,"aProgType":"PROGNOSED","dInR":true,"aTimeS":"115700","dProdX":0,"dPlatfS":"3","dTZOffset":60,"aTimeR":"115700","dTrnCmpSX":{"tcocX":[28,29]},"dProgType":"PROGNOSED","type":"N","dTimeS":"115900","aProdX":0,"aPlatfS":"3","aTZOffset":60,"aOutR":true,"idx":15},{"border":true,"dPlatfS":"5","aTimeS":"122500","dProdX":1,"dTZOffset":60,"locX":1,"dTimeR":"122900","aProgType":"PROGNOSED","dInR":true,"aPlatfS":"5","dTimeS":"122900","aProdX":0,"idx":16,"aTZOffset":60,"aOutR":true,"aTimeR":"122700","type":"N","dProgType":"PROGNOSED"},{"dTimeS":"132800","aProdX":1,"aPlatfS":"8A-F","aOutR":true,"idx":17,"aTZOffset":60,"aTimeR":"132600","dProgType":"PROGNOSED","type":"N","aTimeS":"132600","dProdX":1,"dPlatfS":"8A-F","dTZOffset":60,"dTimeR":"132800","locX":18,"aProgType":"PROGNOSED","dInR":true},{"locX":19,"dTimeR":"141600","dInR":true,"aProgType":"PROGNOSED","dPlatfS":"3","dProdX":1,"aTimeS":"141400","dTZOffset":60,"aTimeR":"141400","type":"N","dProgType":"PROGNOSED","aPlatfS":"3","aProdX":1,"dTimeS":"141600","aOutR":true,"idx":18,"aTZOffset":60},{"dPlatfS":"7","dProdX":1,"aTimeS":"144000","dTZOffset":60,"locX":20,"dTimeR":"144200","dInR":true,"aProgType":"PROGNOSED","aPlatfS":"7","aProdX":1,"dTimeS":"144200","idx":19,"aTZOffset":60,"aOutR":true,"aTimeR":"144000","type":"N","dProgType":"PROGNOSED"},{"aPlatfS":"7A-B","aTimeS":"144700","aProdX":1,"aTZOffset":60,"aOutR":true,"idx":20,"locX":2,"aTimeR":"144700","type":"N","aProgType":"PROGNOSED"}],"jid":"1|196351|0|81|17122023","dTrnCmpSX":{"tcocX":[30,31]},"isRchbl":true,"status":"P","prodX":0}},"err":"OK","meth":"JourneyDetails"}],"ext":"DB.R21.12.a"} diff --git a/t/in/DB.ICE23.json b/t/in/DB.ICE23.json new file mode 100644 index 0000000..44fce25 --- /dev/null +++ b/t/in/DB.ICE23.json @@ -0,0 +1 @@ +{"svcResL":[{"res":{"common":{"opL":[{"icoX":0,"name":"DB Fernverkehr AG"}],"remL":[],"polyL":[],"icoL":[{"res":"D","txt":"DB Fernverkehr AG"},{"res":"ICE"},{"res":"EST"}],"prodL":[{"prodCtx":{"catOutL":"Intercity-Express","catIn":"ICE","catOutS":"ICE","lineId":"0_80_____91","name":"ICE 23","admin":"80____","line":"91","catOut":"ICE","num":"23","catCode":"0","matchId":"91"},"number":"23","nameS":"91","oprX":0,"cls":1,"name":"ICE 23","icoX":1}],"locL":[{"lid":"A=1@O=Dortmund Hbf@X=7459294@Y=51517899@U=81@L=8000080@","crd":{"type":"WGS84","y":51517872,"crdSysX":0,"z":0,"x":7459276,"layerX":0},"state":"F","pCls":191,"extId":"8000080","name":"Dortmund Hbf","icoX":2,"type":"S"},{"crd":{"y":48573778,"type":"WGS84","x":13450775,"layerX":0,"z":0,"crdSysX":0},"lid":"A=1@O=Passau Hbf@X=13450775@Y=48573635@U=81@L=8000298@","state":"F","pCls":555,"extId":"8000298","name":"Passau Hbf","icoX":1,"type":"S"}],"layerL":[{"annoCnt":0,"id":"standard","index":0,"name":"standard"}],"crdSysL":[{"id":"standard","dim":3,"type":"WGS84","index":0}]},"jnyL":[{"date":"20231217","pos":{"x":9530301,"y":50034148,"layerX":0,"crdSysX":0},"stopL":[{"dTZOffset":60,"dTimeS":"043400","locX":0},{"aTimeS":"122500","aTZOffset":60,"locX":1}],"prodX":0,"sDaysL":[{"sDaysR":"fährt 16. Dez 2023 bis 15. Jul 2024; nicht 25., 26., 28., 29. Dez 2023, 1. Jan bis 16. Feb 2024, 23. Mär bis 7. Apr 2024 ","sDaysB":"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003FE4C000000000007FFFFFFFF0000FFFFFFFFFFFFFFFFFFFFFFFFE00000000000000000000000000000000000000"}],"jid":"1|196351|0|81|17122023"}],"fpE":"20241214","fpB":"20221211","planrtTS":"1702800532"},"meth":"JourneyMatch","err":"OK"}],"ext":"DB.R21.12.a","cInfo":{"code":"OK"},"id":"8gmi5je6m2cywg8g","lang":"deu","ver":"1.15"} diff --git a/t/in/DB.ICE615.journey.json b/t/in/DB.ICE615.journey.json new file mode 100644 index 0000000..0bc9715 --- /dev/null +++ b/t/in/DB.ICE615.journey.json @@ -0,0 +1 @@ +{"ver":"1.15","cInfo":{"code":"OK"},"lang":"deu","svcResL":[{"err":"OK","meth":"JourneyDetails","res":{"fpB":"20221211","journey":{"subscr":"F","stopL":[{"dTimeS":"152900","dDirFlg":"1","type":"N","idx":0,"dProgType":"PROGNOSED","dProdX":0,"msgL":[{"type":"REM","tagL":["RES_LOC_H3"],"remX":6}],"dTZOffset":60,"locX":0,"dInR":true,"dDirTxt":"München Hbf","dPlatfS":"12"},{"type":"N","dTimeS":"153800","dProdX":0,"aTZOffset":60,"idx":1,"aOutS":false,"dInR":true,"dTZOffset":60,"aProdX":0,"locX":4,"aOutR":false,"dPlatfS":"4"},{"type":"N","dTimeS":"154500","aTZOffset":60,"dProdX":0,"aOutS":false,"idx":2,"dInR":true,"locX":5,"aProdX":0,"dTZOffset":60,"aOutR":false,"dPlatfS":"12"},{"aOutR":false,"dPlatfS":"4","dInR":true,"locX":6,"aProdX":0,"dTZOffset":60,"aTZOffset":60,"dProdX":0,"aOutS":false,"idx":3,"type":"N","dTimeS":"155700","dTrnCmpSX":{"tcocX":[0,1]}},{"dTrnCmpSX":{"tcocX":[2,3]},"dTimeS":"164400","type":"N","aPlatfS":"10","idx":4,"aTZOffset":60,"msgL":[{"type":"REM","tagL":["RES_LOC_H3"],"remX":7}],"dProdX":0,"locX":1,"aProdX":0,"dTZOffset":60,"dInR":true,"dDirTxt":"Frankfurt(M) Flughafen Fernbf","dPlatfS":"10","aTimeS":"164100","aOutR":true},{"dInR":true,"dDirTxt":"München Hbf","locX":2,"aProdX":0,"dTZOffset":60,"aTimeS":"173500","aOutR":true,"dPlatfS":"2","type":"N","aPlatfS":"2","dTimeS":"173700","dTrnCmpSX":{"tcocX":[4,5]},"aTZOffset":60,"msgL":[{"type":"REM","remX":6,"tagL":["RES_LOC_H3"]}],"dProdX":0,"idx":5},{"aOutR":true,"aTimeS":"180000","dPlatfS":"9","dInR":true,"dTZOffset":60,"locX":7,"aProdX":0,"dProdX":0,"aTZOffset":60,"idx":6,"aPlatfS":"9","type":"N","dTrnCmpSX":{"tcocX":[6,7]},"dTimeS":"180200"},{"dProdX":0,"aTZOffset":60,"idx":7,"aPlatfS":"11","type":"N","dTrnCmpSX":{"tcocX":[8,9]},"dTimeS":"183600","aOutR":true,"aTimeS":"183200","dPlatfS":"11","dInR":true,"dTZOffset":60,"locX":8,"aProdX":0},{"aProdX":0,"locX":9,"dTZOffset":60,"dInR":true,"dPlatfS":"7/8","aOutR":true,"aTimeS":"185500","dTimeS":"185700","dTrnCmpSX":{"tcocX":[10,11]},"type":"N","aPlatfS":"7/8","idx":8,"aTZOffset":60,"dProdX":0},{"dPlatfS":"1","aOutR":true,"aTimeS":"191200","dTZOffset":60,"aProdX":0,"locX":10,"dInR":true,"idx":9,"dProdX":0,"aTZOffset":60,"dTimeS":"191400","dTrnCmpSX":{"tcocX":[12,13]},"aPlatfS":"1","type":"N"},{"aTZOffset":60,"dProdX":0,"idx":10,"type":"N","aPlatfS":"1","dTimeS":"193000","dTrnCmpSX":{"tcocX":[14,15]},"aOutR":true,"aTimeS":"192800","dPlatfS":"1","dInR":true,"locX":11,"aProdX":0,"dTZOffset":60},{"dInR":true,"locX":12,"aProdX":0,"dTZOffset":60,"aTimeS":"194600","aOutR":true,"dPlatfS":"11","type":"N","aPlatfS":"11","dTimeS":"195100","dTrnCmpSX":{"tcocX":[16,17]},"aTZOffset":60,"dProdX":0,"idx":11},{"dInR":true,"locX":13,"aProdX":0,"dTZOffset":60,"aOutR":true,"aTimeS":"205000","dPlatfS":"Fern 5","type":"N","aPlatfS":"Fern 5","dTrnCmpSX":{"tcocX":[18,19]},"dTimeS":"205100","aTZOffset":60,"dProdX":0,"idx":12},{"dTrnCmpSX":{"tcocX":[20,21]},"dTimeS":"213100","aPlatfS":"7","type":"N","idx":13,"dProdX":0,"aTZOffset":60,"dTZOffset":60,"aProdX":0,"locX":14,"dInR":true,"dPlatfS":"7","aTimeS":"212400","aOutR":true},{"idx":14,"aTZOffset":60,"dProdX":0,"dTrnCmpSX":{"tcocX":[22,23]},"dTimeS":"221400","type":"N","aPlatfS":"16","dPlatfS":"16","aOutR":true,"aTimeS":"220800","aProdX":0,"locX":15,"dTZOffset":60,"dInR":true},{"type":"N","aPlatfS":"2","dTimeS":"231200","dTrnCmpSX":{"tcocX":[24,25]},"aTZOffset":60,"dProdX":0,"idx":15,"dInR":true,"locX":16,"aProdX":0,"dTZOffset":60,"aOutR":true,"aTimeS":"231000","dPlatfS":"2"},{"type":"N","aPlatfS":"4","dTrnCmpSX":{"tcocX":[26,27]},"dTimeS":"235500","aTZOffset":60,"dProdX":0,"idx":16,"dInR":true,"locX":17,"aProdX":0,"dTZOffset":60,"aTimeS":"235300","aOutR":true,"dPlatfS":"4"},{"locX":18,"aProdX":0,"dTZOffset":60,"dInR":false,"aTimeS":"01001700","aOutR":true,"type":"N","aPlatfS":"9","idx":17,"aTZOffset":60,"dProdX":0,"dInS":false},{"idx":18,"aOutR":true,"aTimeS":"01002800","aTZOffset":60,"locX":3,"aProdX":0,"aProgType":"PROGNOSED","aPlatfS":"15","type":"N"}],"dirTxt":"München Hbf","isRchbl":true,"jid":"1|160139|0|81|17122023","prodX":0,"dirFlg":"1","msgL":[{"tIdx":18,"remX":0,"fLocX":0,"tagL":["RES_JNY_DTL"],"type":"REM","tLocX":3,"fIdx":0},{"fIdx":0,"type":"REM","tLocX":3,"tagL":["RES_JNY_DTL"],"fLocX":0,"remX":1,"tIdx":18},{"remX":2,"tIdx":18,"fLocX":0,"type":"REM","tLocX":3,"tagL":["RES_JNY_DTL"],"fIdx":0},{"tLocX":3,"type":"REM","tagL":["RES_JNY_DTL"],"fIdx":0,"tIdx":18,"remX":3,"fLocX":0},{"fIdx":0,"type":"REM","tLocX":3,"tagL":["RES_JNY_DTL"],"fLocX":0,"tIdx":18,"remX":4},{"tIdx":-1,"remX":5,"prio":120,"tagL":["RES_JNY_H2"],"type":"REM","fIdx":-1},{"type":"REM","tagL":["SUM_JNY_H3"],"remX":8},{"remX":9,"tagL":["SUM_JNY_H3"],"type":"REM"}],"date":"20231217","sDaysL":[{"fLocX":0,"tLocX":3,"sDaysB":"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003F000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","sDaysR":"fährt 17. bis 21. Dez 2023 "}],"status":"P","dTrnCmpSX":{"tcocX":[28,29]}},"common":{"prodL":[{"number":"615","prodCtx":{"catCode":"0","matchId":"42","num":"615","catOutL":"Intercity-Express","catOut":"ICE","admin":"80____","name":"ICE 615","catIn":"ICE","catOutS":"ICE"},"name":"ICE 615","cls":1,"oprX":0,"icoX":0}],"locL":[{"crd":{"y":53552571,"z":0,"layerX":0,"x":9934860,"crdSysX":0,"type":"WGS84"},"pCls":63,"type":"S","extId":"8002553","icoX":0,"lid":"A=1@O=Hamburg-Altona@X=9935175@Y=53552697@U=81@L=8002553@","state":"F","name":"Hamburg-Altona"},{"state":"F","name":"Bremen Hbf","crd":{"y":53083280,"z":0,"type":"WGS84","layerX":0,"x":8813681,"crdSysX":0},"type":"S","pCls":319,"extId":"8000050","icoX":0,"lid":"A=1@O=Bremen Hbf@X=8813833@Y=53083478@U=81@L=8000050@"},{"lid":"A=1@O=Osnabrück Hbf@X=8061778@Y=52272849@U=81@L=8000294@","icoX":0,"type":"S","extId":"8000294","pCls":47,"crd":{"crdSysX":0,"x":8061256,"layerX":0,"type":"WGS84","z":0,"y":52272534},"name":"Osnabrück Hbf","state":"F"},{"state":"F","name":"München Hbf","crd":{"type":"WGS84","crdSysX":0,"x":11558744,"layerX":0,"z":0,"y":48140364},"extId":"8000261","pCls":447,"type":"S","icoX":2,"lid":"A=1@O=München Hbf@X=11558339@Y=48140229@U=81@L=8000261@"},{"name":"Hamburg Dammtor","state":"F","lid":"A=1@O=Hamburg Dammtor@X=9989569@Y=53560751@U=81@L=8002548@","icoX":0,"type":"S","extId":"8002548","pCls":59,"crd":{"y":53560841,"z":0,"type":"WGS84","layerX":0,"x":9989533,"crdSysX":0}},{"state":"F","name":"Hamburg Hbf","crd":{"y":53553533,"z":0,"type":"WGS84","x":10006360,"layerX":0,"crdSysX":0},"extId":"8002549","type":"S","pCls":191,"icoX":0,"lid":"A=1@O=Hamburg Hbf@X=10006909@Y=53552733@U=81@L=8002549@"},{"icoX":0,"lid":"A=1@O=Hamburg-Harburg@X=9991699@Y=53455910@U=81@L=8000147@","crd":{"type":"WGS84","x":9991591,"layerX":0,"crdSysX":0,"y":53456198,"z":0},"type":"S","pCls":63,"extId":"8000147","name":"Hamburg-Harburg","state":"F"},{"icoX":0,"lid":"A=1@O=Münster(Westf)Hbf@X=7635716@Y=51956563@U=81@L=8000263@","crd":{"y":51956527,"z":0,"type":"WGS84","layerX":0,"x":7635860,"crdSysX":0},"type":"S","extId":"8000263","pCls":559,"name":"Münster(Westf)Hbf","state":"F"},{"icoX":3,"lid":"A=1@O=Dortmund Hbf@X=7459294@Y=51517899@U=81@L=8000080@","crd":{"type":"WGS84","x":7459276,"layerX":0,"crdSysX":0,"y":51517872,"z":0},"extId":"8000080","pCls":191,"type":"S","name":"Dortmund Hbf","state":"F"},{"name":"Hagen Hbf","state":"F","icoX":0,"lid":"A=1@O=Hagen Hbf@X=7460247@Y=51362745@U=81@L=8000142@","crd":{"type":"WGS84","crdSysX":0,"x":7460391,"layerX":0,"z":0,"y":51362727},"type":"S","extId":"8000142","pCls":59},{"lid":"A=1@O=Wuppertal Hbf@X=7149544@Y=51254362@U=81@L=8000266@","icoX":0,"pCls":315,"extId":"8000266","type":"S","crd":{"type":"WGS84","crdSysX":0,"layerX":0,"x":7150155,"z":0,"y":51254443},"name":"Wuppertal Hbf","state":"F"},{"state":"F","name":"Solingen Hbf","pCls":571,"type":"S","extId":"8000087","crd":{"crdSysX":0,"x":7004287,"layerX":0,"type":"WGS84","z":0,"y":51160784},"lid":"A=1@O=Solingen Hbf@X=7004188@Y=51160766@U=81@L=8000087@","icoX":0},{"extId":"8073368","type":"S","pCls":319,"crd":{"y":50940602,"z":0,"x":6975162,"layerX":0,"crdSysX":0,"type":"WGS84"},"entry":true,"lid":"A=1@O=Köln Messe/Deutz Gl.11-12@X=6974065@Y=50941717@U=81@L=8073368@","icoX":0,"state":"F","name":"Köln Messe/Deutz Gl.11-12"},{"lid":"A=1@O=Frankfurt(M) Flughafen Fernbf@X=8570181@Y=50053169@U=81@L=8070003@","icoX":0,"extId":"8070003","type":"S","pCls":31,"crd":{"type":"WGS84","crdSysX":0,"x":8569776,"layerX":0,"z":0,"y":50052926},"name":"Frankfurt(M) Flughafen Fernbf","state":"F"},{"name":"Mannheim Hbf","state":"F","icoX":2,"lid":"A=1@O=Mannheim Hbf@X=8468917@Y=49479352@U=81@L=8000244@","crd":{"z":0,"y":49479181,"crdSysX":0,"x":8469268,"layerX":0,"type":"WGS84"},"extId":"8000244","pCls":319,"type":"S"},{"lid":"A=1@O=Stuttgart Hbf@X=9181636@Y=48784081@U=81@L=8000096@","icoX":0,"extId":"8000096","type":"S","pCls":319,"crd":{"layerX":0,"x":9182589,"crdSysX":0,"type":"WGS84","y":48785052,"z":0},"name":"Stuttgart Hbf","state":"F"},{"name":"Ulm Hbf","state":"F","icoX":0,"lid":"A=1@O=Ulm Hbf@X=9982224@Y=48399433@U=81@L=8000170@","crd":{"z":0,"y":48399585,"crdSysX":0,"layerX":0,"x":9982422,"type":"WGS84"},"pCls":303,"extId":"8000170","type":"S"},{"name":"Augsburg Hbf","state":"F","lid":"A=1@O=Augsburg Hbf@X=10885568@Y=48365444@U=81@L=8000013@","icoX":0,"extId":"8000013","type":"S","pCls":303,"crd":{"type":"WGS84","layerX":0,"x":10885595,"crdSysX":0,"y":48365247,"z":0}},{"name":"München-Pasing","state":"F","icoX":0,"lid":"A=1@O=München-Pasing@X=11461876@Y=48149856@U=81@L=8004158@","crd":{"y":48150036,"z":0,"layerX":0,"x":11461633,"crdSysX":0,"type":"WGS84"},"pCls":319,"extId":"8004158","type":"S"}],"icoL":[{"res":"ICE"},{"res":"D","txt":"DB Fernverkehr AG"},{"res":"ECE"},{"res":"EST"},{"res":"attr_info"},{"res":"attr_bike_r"},{"res":"attr_bike"},{"res":"attr_resto"},{"res":"HimInfo"}],"tcocL":[{"c":"FIRST","r":2},{"r":3,"c":"SECOND"},{"r":2,"c":"FIRST"},{"r":3,"c":"SECOND"},{"r":2,"c":"FIRST"},{"c":"SECOND","r":3},{"c":"FIRST","r":2},{"r":3,"c":"SECOND"},{"r":2,"c":"FIRST"},{"r":2,"c":"SECOND"},{"r":2,"c":"FIRST"},{"c":"SECOND","r":2},{"c":"FIRST","r":2},{"r":2,"c":"SECOND"},{"c":"FIRST","r":2},{"r":2,"c":"SECOND"},{"c":"FIRST","r":2},{"r":2,"c":"SECOND"},{"c":"FIRST","r":2},{"r":2,"c":"SECOND"},{"c":"FIRST","r":1},{"r":2,"c":"SECOND"},{"c":"FIRST","r":1},{"r":1,"c":"SECOND"},{"c":"FIRST","r":1},{"r":1,"c":"SECOND"},{"c":"FIRST","r":1},{"r":1,"c":"SECOND"},{"r":2,"c":"FIRST"},{"c":"SECOND","r":3}],"crdSysL":[{"dim":3,"id":"standard","type":"WGS84","index":0}],"layerL":[{"annoCnt":0,"name":"standard","index":0,"id":"standard"}],"opL":[{"name":"DB Fernverkehr AG","icoX":1}],"polyL":[],"remL":[{"type":"A","code":"CK","icoX":4,"prio":200,"txtN":"Sicher & kontaktlos ohne Ticketkontrolle reisen mit Komfort Check-in"},{"code":"FR","type":"A","icoX":5,"prio":260,"txtN":"Fahrradmitnahme reservierungspflichtig"},{"txtN":"Fahrradmitnahme begrenzt möglich","icoX":6,"prio":260,"code":"FB","type":"A"},{"icoX":7,"prio":450,"txtN":"Bordrestaurant","code":"BR","type":"A"},{"prio":560,"icoX":4,"txtN":"Fahrzeuggebundene Einstiegshilfe vorhanden","code":"EH","type":"A"},{"code":"","txtS":"Hamburg-Altona->München Hbf: Information. ","type":"M","icoX":8,"txtN":"Keine fahrzeuggebundene Einstiegshilfe. Mobilitätseingeschränkte Reisende wenden sich bzgl. eventuell erforderlicher Umbuchungen an unsere Mobilitätsservice-Zentrale unter 030 65212888."},{"txtN":"Verkehrt ab hier als ICE 615 in Richtung München Hbf","icoX":4,"code":"text.journeystop.product.or.direction.changes.stop.message","type":"A"},{"code":"text.journeystop.product.or.direction.changes.stop.message","type":"A","txtN":"Verkehrt ab hier als ICE 615 in Richtung Frankfurt(M) Flughafen Fernbf","icoX":4},{"code":"text.journeystop.product.or.direction.changes.journey.message","type":"A","txtN":"Verkehrt ab Bremen Hbf als ICE 615 in Richtung Frankfurt(M) Flughafen Fernbf","icoX":4},{"code":"text.journeystop.product.or.direction.changes.journey.message","type":"A","icoX":4,"txtN":"Verkehrt ab Osnabrück Hbf als ICE 615 in Richtung München Hbf"}]},"fpE":"20241214","planrtTS":"1702821532"}}],"ext":"DB.R21.12.a","id":"cpk2zjackqe8pics"} diff --git a/t/00-compile-pm.t b/xt/00-compile-pm.t index 2476ab2..2476ab2 100755 --- a/t/00-compile-pm.t +++ b/xt/00-compile-pm.t diff --git a/t/01-compile-pl.t b/xt/01-compile-pl.t index f130ac4..f130ac4 100755 --- a/t/01-compile-pl.t +++ b/xt/01-compile-pl.t diff --git a/t/10-pod-coverage.t b/xt/10-pod-coverage.t index 5fe4faa..5fe4faa 100755 --- a/t/10-pod-coverage.t +++ b/xt/10-pod-coverage.t |