summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/perl.yml33
-rw-r--r--.mailmap1
-rw-r--r--.travis.yml9
-rw-r--r--[-rwxr-xr-x]Build.PL32
-rw-r--r--COPYING2
-rw-r--r--Changelog246
-rw-r--r--Dockerfile23
-rw-r--r--README44
-rw-r--r--README.md182
-rwxr-xr-xbin/hafas-m731
-rw-r--r--cpanfile17
-rw-r--r--lib/Travel/Status/DE/DeutscheBahn.pm20
-rw-r--r--lib/Travel/Status/DE/HAFAS.pm1434
-rw-r--r--lib/Travel/Status/DE/HAFAS/Journey.pm682
-rw-r--r--lib/Travel/Status/DE/HAFAS/Location.pm127
-rw-r--r--lib/Travel/Status/DE/HAFAS/Message.pm185
-rw-r--r--lib/Travel/Status/DE/HAFAS/Polyline.pm97
-rw-r--r--lib/Travel/Status/DE/HAFAS/Product.pm195
-rw-r--r--lib/Travel/Status/DE/HAFAS/Result.pm319
-rw-r--r--lib/Travel/Status/DE/HAFAS/Stop.pm368
-rw-r--r--lib/Travel/Status/DE/HAFAS/StopFinder.pm112
-rwxr-xr-xscripts/check-hafas-urls22
-rwxr-xr-xscripts/makedeb-docker11
-rwxr-xr-xscripts/makedeb-docker-helper34
-rwxr-xr-xscripts/update-readme20
-rwxr-xr-xt/20-db.t368
-rwxr-xr-xt/21-db-journeymatch.t84
-rwxr-xr-xt/22-db-journey.t336
-rwxr-xr-xt/30-db-journey-platformchange.t75
-rwxr-xr-xt/31-db-journey-daychange.t88
-rw-r--r--t/in/DB.Berlin Jannowitzbrücke.json1
-rw-r--r--t/in/DB.EC392.journey.json1
-rw-r--r--t/in/DB.ICE23.journey.json1
-rw-r--r--t/in/DB.ICE23.json1
-rw-r--r--t/in/DB.ICE615.journey.json1
-rwxr-xr-xxt/00-compile-pm.t (renamed from t/00-compile-pm.t)0
-rwxr-xr-xxt/01-compile-pl.t (renamed from t/01-compile-pl.t)0
-rwxr-xr-xxt/10-pod-coverage.t (renamed from t/10-pod-coverage.t)0
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"
diff --git a/Build.PL b/Build.PL
index ffea08c..140ffad 100755..100644
--- a/Build.PL
+++ b/Build.PL
@@ -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();
diff --git a/COPYING b/COPYING
index 310b8e4..af9a3cc 100644
--- a/COPYING
+++ b/COPYING
@@ -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.
diff --git a/Changelog b/Changelog
index 12a8a37..0b128a5 100644
--- a/Changelog
+++ b/Changelog
@@ -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"]
diff --git a/README b/README
deleted file mode 100644
index a5bcfa5..0000000
--- a/README
+++ /dev/null
@@ -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