summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.mailmap1
-rw-r--r--Dockerfile4
-rw-r--r--README.md25
-rw-r--r--cpanfile10
-rw-r--r--cpanfile.snapshot1712
-rw-r--r--lib/DBInfoscreen.pm133
-rw-r--r--lib/DBInfoscreen/Controller/Map.pm1038
-rw-r--r--lib/DBInfoscreen/Controller/Static.pm8
-rw-r--r--lib/DBInfoscreen/Controller/Stationboard.pm1823
-rw-r--r--lib/DBInfoscreen/Controller/Wagenreihung.pm220
-rw-r--r--lib/DBInfoscreen/Helper/DBRIS.pm93
-rw-r--r--lib/DBInfoscreen/Helper/EFA.pm53
-rw-r--r--lib/DBInfoscreen/Helper/HAFAS.pm369
-rw-r--r--lib/DBInfoscreen/Helper/MOTIS.pm82
-rw-r--r--lib/DBInfoscreen/Helper/Wagonorder.pm205
-rw-r--r--lib/DBInfoscreen/I18N/en.pm84
-rw-r--r--public/static/css/dark.min.css2
-rw-r--r--public/static/css/legacy-mobile.css (renamed from public/static/css/mobile.css)0
-rw-r--r--public/static/css/legacy.css (renamed from public/static/css/default.css)0
-rw-r--r--public/static/css/light.min.css2
-rw-r--r--public/static/css/material-icons.css8
-rw-r--r--public/static/js/collapse.js41
-rw-r--r--public/static/js/dbf.min.js2
-rw-r--r--public/static/js/geostop.js14
-rw-r--r--public/static/js/geostop.min.js2
-rw-r--r--public/static/js/map-refresh.js10
-rw-r--r--public/static/js/map-refresh.min.js2
l---------public/static/v109 (renamed from public/static/v84)0
l---------public/static/v110 (renamed from public/static/v85)0
-rw-r--r--sass/app.scss142
-rw-r--r--sass/dark.scss6
-rw-r--r--sass/light.scss6
-rwxr-xr-xscripts/asset-release2
-rw-r--r--templates/_intersection_infobox.html.ep22
-rw-r--r--templates/_map_infobox.html.ep2
-rw-r--r--templates/_train_details.html.ep249
-rw-r--r--templates/_wagon.html.ep52
-rw-r--r--templates/about.html.ep65
-rw-r--r--templates/app.html.ep52
-rw-r--r--templates/coverage_map.html.ep22
-rw-r--r--templates/exception.html.ep2
-rw-r--r--templates/landingpage.html.ep33
-rw-r--r--templates/layouts/app.html.ep182
-rw-r--r--templates/layouts/legacy.html.ep170
-rw-r--r--templates/route_map.html.ep17
-rw-r--r--templates/select_backend.html.ep46
-rw-r--r--templates/trainsearch.html.ep29
-rw-r--r--templates/wagenreihung.html.ep72
48 files changed, 4718 insertions, 2396 deletions
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/Dockerfile b/Dockerfile
index 7cd702e..210893b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -21,7 +21,7 @@ RUN ln -sf ../ext-templates/imprint.html.ep templates/imprint.html.ep \
RUN sed -i "s/version *=> *\$ENV{DBFAKEDISPLAY_VERSION}/version => '${dbf_version}'/" lib/DBInfoscreen.pm
-FROM perl:5.30-slim
+FROM perl:5.40-slim
ARG DEBIAN_FRONTEND=noninteractive
ARG APT_LISTCHANGES_FRONTEND=none
@@ -37,7 +37,7 @@ RUN apt-get update \
libc6-dev \
libdb5.3 \
libdb5.3-dev \
- libssl1.1 \
+ libssl3 \
libssl-dev \
libxml2 \
libxml2-dev \
diff --git a/README.md b/README.md
index b160c3b..7ba7c40 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,12 @@
db-infoscreen - App/Infoscreen for Railway Departures in Germany
---
-[db-infoscreen homepage](https://finalrewind.org/projects/db-fakedisplay/)
-
-db-infoscreen (formerly db-fakedisplay) shows departures at german train
-stations, serving both as infoscreen / webapp and station board look-alike.
-
-It aims to aggregate departure and train data from different sources and
-combine them in a useful (and user-friendly) manner. It is intended both for a
-quick glance at the departure board and for public transportation geeks looking
-for details about specific trains.
+[db-infoscreen](https://finalrewind.org/projects/db-fakedisplay/) (formerly
+db-fakedisplay) shows departures at public transit stops in most of Germany,
+Switzerland, Austria, Luxembourg, Ireland, and parts of the USA. It can serve
+both as infoscreen and web application for mobile usage. Depending on backend
+support, it can provide details on individual departures such as a map of the
+scheduled route, expected occupancy, and carriage formation.
There's a public [db-infoscreen service on
finalrewind.org](https://dbf.finalrewind.org/). You can also host your own
@@ -96,7 +93,6 @@ In hypnotoad mode (recommended), db-infoscreen respects the following environmen
| :------- | :------ | :---------- |
| DBFAKEDISPLAY\_LISTEN | `http://*:8092` | IP and Port for web service |
| DBFAKEDISPLAY\_STATS | _None_ | File in which the total count of backend API requests (excluding those answered from cache) is written |
-| DBFAKEDISPLAY\_HAFAS\_API | `https://v5.db.transport.rest` | hafas-rest-api endpoint |
| DBFAKEDISPLAY\_IRIS\_CACHE | `/tmp/dbf-iris-mian` | Directory for IRIS schedule cache |
| DBFAKEDISPLAY\_IRISRT\_CACHE | `/tmp/dbf-iris-realtime` | Directory for IRIS realtime cache |
| DBFAKEDISPLAY\_WORKERS | 2 | Number of worker processes (i.e., maximum amount of concurrent requests) |
@@ -171,3 +167,12 @@ The easiest way of making changes available is by maintaining a public fork of
the Git repository. A tarball is also acceptable. Please change `source_url` in
`lib/DBInfoscreen.pm` to point to your Git repository / source archive if you
are using a version with custom changes.
+
+Resources
+---
+
+Mirrors of the db-infoscreen repository are available at
+
+* [Chaosdorf](https://chaosdorf.de/git/derf/db-infoscreen)
+* [git.finalrewind.org](https://git.finalrewind.org/db-fakedisplay/)
+* [GitHub](https://github.com/derf/db-fakedisplay)
diff --git a/cpanfile b/cpanfile
index e493208..95ce624 100644
--- a/cpanfile
+++ b/cpanfile
@@ -4,13 +4,15 @@ requires 'DateTime::Format::Strptime';
requires 'File::Slurp';
requires 'GIS::Distance';
requires 'GIS::Distance::Fast';
+requires 'IO::Socket::Socks', '>= 0.64';
+requires 'IO::Socket::SSL', '>= 2.009';
requires 'JSON';
requires 'JSON::XS';
requires 'List::UtilsBy';
-requires 'LWP::UserAgent';
-requires 'LWP::Protocol::https';
requires 'Mojolicious';
-requires 'Travel::Status::DE::DBWagenreihung', '0.06';
-requires 'Travel::Status::DE::HAFAS', '4.00';
+requires 'Travel::Status::DE::DBRIS', '>= 0.06';
+requires 'Travel::Status::DE::EFA', '>= 3.13';
+requires 'Travel::Status::DE::HAFAS', '>= 5.06';
requires 'Travel::Status::DE::IRIS';
+requires 'Travel::Status::MOTIS';
requires 'XML::LibXML';
diff --git a/cpanfile.snapshot b/cpanfile.snapshot
index 1606054..6ee75bf 100644
--- a/cpanfile.snapshot
+++ b/cpanfile.snapshot
@@ -1,87 +1,87 @@
# carton snapshot format: version 1.0
DISTRIBUTIONS
- Alien-Build-2.80
- pathname: P/PL/PLICEASE/Alien-Build-2.80.tar.gz
- provides:
- Alien::Base 2.80
- Alien::Base::PkgConfig 2.80
- Alien::Base::Wrapper 2.80
- Alien::Build 2.80
- Alien::Build::CommandSequence 2.80
- Alien::Build::Helper 2.80
- Alien::Build::Interpolate 2.80
- Alien::Build::Interpolate::Default 2.80
- Alien::Build::Interpolate::Helper 2.80
- Alien::Build::Log 2.80
- Alien::Build::Log::Abbreviate 2.80
- Alien::Build::Log::Default 2.80
- Alien::Build::MM 2.80
- Alien::Build::Meta 2.80
- Alien::Build::Plugin 2.80
- Alien::Build::Plugin::Build::Autoconf 2.80
- Alien::Build::Plugin::Build::CMake 2.80
- Alien::Build::Plugin::Build::Copy 2.80
- Alien::Build::Plugin::Build::MSYS 2.80
- Alien::Build::Plugin::Build::Make 2.80
- Alien::Build::Plugin::Build::SearchDep 2.80
- Alien::Build::Plugin::Core::CleanInstall 2.80
- Alien::Build::Plugin::Core::Download 2.80
- Alien::Build::Plugin::Core::FFI 2.80
- Alien::Build::Plugin::Core::Gather 2.80
- Alien::Build::Plugin::Core::Legacy 2.80
- Alien::Build::Plugin::Core::Override 2.80
- Alien::Build::Plugin::Core::Setup 2.80
- Alien::Build::Plugin::Core::Tail 2.80
- Alien::Build::Plugin::Decode::DirListing 2.80
- Alien::Build::Plugin::Decode::DirListingFtpcopy 2.80
- Alien::Build::Plugin::Decode::HTML 2.80
- Alien::Build::Plugin::Decode::Mojo 2.80
- Alien::Build::Plugin::Digest::Negotiate 2.80
- Alien::Build::Plugin::Digest::SHA 2.80
- Alien::Build::Plugin::Digest::SHAPP 2.80
- Alien::Build::Plugin::Download::Negotiate 2.80
- Alien::Build::Plugin::Extract::ArchiveTar 2.80
- Alien::Build::Plugin::Extract::ArchiveZip 2.80
- Alien::Build::Plugin::Extract::CommandLine 2.80
- Alien::Build::Plugin::Extract::Directory 2.80
- Alien::Build::Plugin::Extract::File 2.80
- Alien::Build::Plugin::Extract::Negotiate 2.80
- Alien::Build::Plugin::Fetch::CurlCommand 2.80
- Alien::Build::Plugin::Fetch::HTTPTiny 2.80
- Alien::Build::Plugin::Fetch::LWP 2.80
- Alien::Build::Plugin::Fetch::Local 2.80
- Alien::Build::Plugin::Fetch::LocalDir 2.80
- Alien::Build::Plugin::Fetch::NetFTP 2.80
- Alien::Build::Plugin::Fetch::Wget 2.80
- Alien::Build::Plugin::Gather::IsolateDynamic 2.80
- Alien::Build::Plugin::PkgConfig::CommandLine 2.80
- Alien::Build::Plugin::PkgConfig::LibPkgConf 2.80
- Alien::Build::Plugin::PkgConfig::MakeStatic 2.80
- Alien::Build::Plugin::PkgConfig::Negotiate 2.80
- Alien::Build::Plugin::PkgConfig::PP 2.80
- Alien::Build::Plugin::Prefer::BadVersion 2.80
- Alien::Build::Plugin::Prefer::GoodVersion 2.80
- Alien::Build::Plugin::Prefer::SortVersions 2.80
- Alien::Build::Plugin::Probe::CBuilder 2.80
- Alien::Build::Plugin::Probe::CommandLine 2.80
- Alien::Build::Plugin::Probe::Vcpkg 2.80
- Alien::Build::Plugin::Test::Mock 2.80
- Alien::Build::PluginMeta 2.80
- Alien::Build::Temp 2.80
- Alien::Build::TempDir 2.80
- Alien::Build::Util 2.80
- Alien::Build::Version::Basic 2.80
- Alien::Build::rc 2.80
- Alien::Role 2.80
- Alien::Util 2.80
- Test::Alien 2.80
- Test::Alien::Build 2.80
- Test::Alien::CanCompile 2.80
- Test::Alien::CanPlatypus 2.80
- Test::Alien::Diag 2.80
- Test::Alien::Run 2.80
- Test::Alien::Synthetic 2.80
- alienfile 2.80
+ Alien-Build-2.84
+ pathname: P/PL/PLICEASE/Alien-Build-2.84.tar.gz
+ provides:
+ Alien::Base 2.84
+ Alien::Base::PkgConfig 2.84
+ Alien::Base::Wrapper 2.84
+ Alien::Build 2.84
+ Alien::Build::CommandSequence 2.84
+ Alien::Build::Helper 2.84
+ Alien::Build::Interpolate 2.84
+ Alien::Build::Interpolate::Default 2.84
+ Alien::Build::Interpolate::Helper 2.84
+ Alien::Build::Log 2.84
+ Alien::Build::Log::Abbreviate 2.84
+ Alien::Build::Log::Default 2.84
+ Alien::Build::MM 2.84
+ Alien::Build::Meta 2.84
+ Alien::Build::Plugin 2.84
+ Alien::Build::Plugin::Build::Autoconf 2.84
+ Alien::Build::Plugin::Build::CMake 2.84
+ Alien::Build::Plugin::Build::Copy 2.84
+ Alien::Build::Plugin::Build::MSYS 2.84
+ Alien::Build::Plugin::Build::Make 2.84
+ Alien::Build::Plugin::Build::SearchDep 2.84
+ Alien::Build::Plugin::Core::CleanInstall 2.84
+ Alien::Build::Plugin::Core::Download 2.84
+ Alien::Build::Plugin::Core::FFI 2.84
+ Alien::Build::Plugin::Core::Gather 2.84
+ Alien::Build::Plugin::Core::Legacy 2.84
+ Alien::Build::Plugin::Core::Override 2.84
+ Alien::Build::Plugin::Core::Setup 2.84
+ Alien::Build::Plugin::Core::Tail 2.84
+ Alien::Build::Plugin::Decode::DirListing 2.84
+ Alien::Build::Plugin::Decode::DirListingFtpcopy 2.84
+ Alien::Build::Plugin::Decode::HTML 2.84
+ Alien::Build::Plugin::Decode::Mojo 2.84
+ Alien::Build::Plugin::Digest::Negotiate 2.84
+ Alien::Build::Plugin::Digest::SHA 2.84
+ Alien::Build::Plugin::Digest::SHAPP 2.84
+ Alien::Build::Plugin::Download::Negotiate 2.84
+ Alien::Build::Plugin::Extract::ArchiveTar 2.84
+ Alien::Build::Plugin::Extract::ArchiveZip 2.84
+ Alien::Build::Plugin::Extract::CommandLine 2.84
+ Alien::Build::Plugin::Extract::Directory 2.84
+ Alien::Build::Plugin::Extract::File 2.84
+ Alien::Build::Plugin::Extract::Negotiate 2.84
+ Alien::Build::Plugin::Fetch::CurlCommand 2.84
+ Alien::Build::Plugin::Fetch::HTTPTiny 2.84
+ Alien::Build::Plugin::Fetch::LWP 2.84
+ Alien::Build::Plugin::Fetch::Local 2.84
+ Alien::Build::Plugin::Fetch::LocalDir 2.84
+ Alien::Build::Plugin::Fetch::NetFTP 2.84
+ Alien::Build::Plugin::Fetch::Wget 2.84
+ Alien::Build::Plugin::Gather::IsolateDynamic 2.84
+ Alien::Build::Plugin::PkgConfig::CommandLine 2.84
+ Alien::Build::Plugin::PkgConfig::LibPkgConf 2.84
+ Alien::Build::Plugin::PkgConfig::MakeStatic 2.84
+ Alien::Build::Plugin::PkgConfig::Negotiate 2.84
+ Alien::Build::Plugin::PkgConfig::PP 2.84
+ Alien::Build::Plugin::Prefer::BadVersion 2.84
+ Alien::Build::Plugin::Prefer::GoodVersion 2.84
+ Alien::Build::Plugin::Prefer::SortVersions 2.84
+ Alien::Build::Plugin::Probe::CBuilder 2.84
+ Alien::Build::Plugin::Probe::CommandLine 2.84
+ Alien::Build::Plugin::Probe::Vcpkg 2.84
+ Alien::Build::Plugin::Test::Mock 2.84
+ Alien::Build::PluginMeta 2.84
+ Alien::Build::Temp 2.84
+ Alien::Build::TempDir 2.84
+ Alien::Build::Util 2.84
+ Alien::Build::Version::Basic 2.84
+ Alien::Build::rc 2.84
+ Alien::Role 2.84
+ Alien::Util 2.84
+ Test::Alien 2.84
+ Test::Alien::Build 2.84
+ Test::Alien::CanCompile 2.84
+ Test::Alien::CanPlatypus 2.84
+ Test::Alien::Diag 2.84
+ Test::Alien::Run 2.84
+ Test::Alien::Synthetic 2.84
+ alienfile 2.84
requirements:
Capture::Tiny 0.17
Digest::SHA 0
@@ -94,6 +94,7 @@ DISTRIBUTIONS
JSON::PP 0
List::Util 1.33
Path::Tiny 0.077
+ PkgConfig 0.14026
Test2::API 1.302096
Text::ParseWords 3.26
parent 0
@@ -110,10 +111,10 @@ DISTRIBUTIONS
URI 0
URI::Escape 0
perl 5.008004
- Alien-Libxml2-0.19
- pathname: P/PL/PLICEASE/Alien-Libxml2-0.19.tar.gz
+ Alien-Libxml2-0.20
+ pathname: P/PL/PLICEASE/Alien-Libxml2-0.20.tar.gz
provides:
- Alien::Libxml2 0.19
+ Alien::Libxml2 0.20
requirements:
Alien::Base 2.37
Alien::Build 2.37
@@ -125,12 +126,12 @@ DISTRIBUTIONS
ExtUtils::CBuilder 0
ExtUtils::MakeMaker 6.52
perl 5.006
- B-Hooks-EndOfScope-0.26
- pathname: E/ET/ETHER/B-Hooks-EndOfScope-0.26.tar.gz
+ B-Hooks-EndOfScope-0.28
+ pathname: E/ET/ETHER/B-Hooks-EndOfScope-0.28.tar.gz
provides:
- B::Hooks::EndOfScope 0.26
- B::Hooks::EndOfScope::PP 0.26
- B::Hooks::EndOfScope::XS 0.26
+ B::Hooks::EndOfScope 0.28
+ B::Hooks::EndOfScope::PP 0.28
+ B::Hooks::EndOfScope::XS 0.28
requirements:
ExtUtils::MakeMaker 0
Hash::Util::FieldHash 0
@@ -198,10 +199,10 @@ DISTRIBUTIONS
Canary::Stability 2013
requirements:
ExtUtils::MakeMaker 0
- Capture-Tiny-0.48
- pathname: D/DA/DAGOLDEN/Capture-Tiny-0.48.tar.gz
+ Capture-Tiny-0.50
+ pathname: D/DA/DAGOLDEN/Capture-Tiny-0.50.tar.gz
provides:
- Capture::Tiny 0.48
+ Capture::Tiny 0.50
requirements:
Carp 0
Exporter 0
@@ -222,10 +223,10 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
base 1.01
- Class-Data-Inheritable-0.09
- pathname: R/RS/RSHERER/Class-Data-Inheritable-0.09.tar.gz
+ Class-Data-Inheritable-0.10
+ pathname: R/RS/RSHERER/Class-Data-Inheritable-0.10.tar.gz
provides:
- Class::Data::Inheritable 0.09
+ Class::Data::Inheritable 0.10
requirements:
ExtUtils::MakeMaker 0
Class-Inspector-1.36
@@ -258,12 +259,23 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
- Clone-0.46
- pathname: G/GA/GARU/Clone-0.46.tar.gz
+ Clone-0.47
+ pathname: A/AT/ATOOMIC/Clone-0.47.tar.gz
provides:
- Clone 0.46
+ Clone 0.47
+ requirements:
+ ExtUtils::MakeMaker 0
+ Clone-PP-1.08
+ pathname: N/NE/NEILB/Clone-PP-1.08.tar.gz
+ provides:
+ Clone::PP 1.08
requirements:
+ Exporter 0
ExtUtils::MakeMaker 0
+ perl 5.006
+ strict 0
+ vars 0
+ warnings 0
Const-Fast-0.014
pathname: L/LE/LEONT/Const-Fast-0.014.tar.gz
provides:
@@ -289,19 +301,19 @@ DISTRIBUTIONS
perl 5.012
strict 0
warnings 0
- DateTime-1.59
- pathname: D/DR/DROLSKY/DateTime-1.59.tar.gz
- provides:
- DateTime 1.59
- DateTime::Duration 1.59
- DateTime::Helpers 1.59
- DateTime::Infinite 1.59
- DateTime::Infinite::Future 1.59
- DateTime::Infinite::Past 1.59
- DateTime::LeapSecond 1.59
- DateTime::PP 1.59
- DateTime::PPExtra 1.59
- DateTime::Types 1.59
+ DateTime-1.66
+ pathname: D/DR/DROLSKY/DateTime-1.66.tar.gz
+ provides:
+ DateTime 1.66
+ DateTime::Duration 1.66
+ DateTime::Helpers 1.66
+ DateTime::Infinite 1.66
+ DateTime::Infinite::Future 1.66
+ DateTime::Infinite::Past 1.66
+ DateTime::LeapSecond 1.66
+ DateTime::PP 1.66
+ DateTime::PPExtra 1.66
+ DateTime::Types 1.66
requirements:
Carp 0
DateTime::Locale 1.06
@@ -311,7 +323,7 @@ DISTRIBUTIONS
POSIX 0
Params::ValidationCompiler 0.26
Scalar::Util 0
- Specio 0.18
+ Specio 0.50
Specio::Declare 0
Specio::Exporter 0
Specio::Library::Builtins 0
@@ -328,6 +340,45 @@ DISTRIBUTIONS
strict 0
warnings 0
warnings::register 0
+ DateTime-Format-Builder-0.83
+ pathname: D/DR/DROLSKY/DateTime-Format-Builder-0.83.tar.gz
+ provides:
+ DateTime::Format::Builder 0.83
+ DateTime::Format::Builder::Parser 0.83
+ DateTime::Format::Builder::Parser::Dispatch 0.83
+ DateTime::Format::Builder::Parser::Quick 0.83
+ DateTime::Format::Builder::Parser::Regex 0.83
+ DateTime::Format::Builder::Parser::Strptime 0.83
+ DateTime::Format::Builder::Parser::generic 0.83
+ requirements:
+ Carp 0
+ DateTime 1.00
+ DateTime::Format::Strptime 1.04
+ ExtUtils::MakeMaker 0
+ Params::Validate 0.72
+ Scalar::Util 0
+ parent 0
+ strict 0
+ warnings 0
+ DateTime-Format-ISO8601-0.17
+ pathname: D/DR/DROLSKY/DateTime-Format-ISO8601-0.17.tar.gz
+ provides:
+ DateTime::Format::ISO8601 0.17
+ DateTime::Format::ISO8601::Types 0.17
+ requirements:
+ Carp 0
+ DateTime 1.45
+ DateTime::Format::Builder 0.77
+ ExtUtils::MakeMaker 0
+ Params::ValidationCompiler 0.26
+ Specio 0.18
+ Specio::Declare 0
+ Specio::Exporter 0
+ Specio::Library::Builtins 0
+ namespace::autoclean 0
+ parent 0
+ strict 0
+ warnings 0
DateTime-Format-Strptime-1.79
pathname: D/DR/DROLSKY/DateTime-Format-Strptime-1.79.tar.gz
provides:
@@ -353,15 +404,15 @@ DISTRIBUTIONS
parent 0
strict 0
warnings 0
- DateTime-Locale-1.39
- pathname: D/DR/DROLSKY/DateTime-Locale-1.39.tar.gz
+ DateTime-Locale-1.45
+ pathname: D/DR/DROLSKY/DateTime-Locale-1.45.tar.gz
provides:
- DateTime::Locale 1.39
- DateTime::Locale::Base 1.39
- DateTime::Locale::Catalog 1.39
- DateTime::Locale::Data 1.39
- DateTime::Locale::FromData 1.39
- DateTime::Locale::Util 1.39
+ DateTime::Locale 1.45
+ DateTime::Locale::Base 1.45
+ DateTime::Locale::Catalog 1.45
+ DateTime::Locale::Data 1.45
+ DateTime::Locale::FromData 1.45
+ DateTime::Locale::Util 1.45
requirements:
Carp 0
Dist::CheckConflicts 0.02
@@ -379,345 +430,335 @@ DISTRIBUTIONS
perl 5.008004
strict 0
warnings 0
- DateTime-TimeZone-2.60
- pathname: D/DR/DROLSKY/DateTime-TimeZone-2.60.tar.gz
- provides:
- DateTime::TimeZone 2.60
- DateTime::TimeZone::Africa::Abidjan 2.60
- DateTime::TimeZone::Africa::Algiers 2.60
- DateTime::TimeZone::Africa::Bissau 2.60
- DateTime::TimeZone::Africa::Cairo 2.60
- DateTime::TimeZone::Africa::Casablanca 2.60
- DateTime::TimeZone::Africa::Ceuta 2.60
- DateTime::TimeZone::Africa::El_Aaiun 2.60
- DateTime::TimeZone::Africa::Johannesburg 2.60
- DateTime::TimeZone::Africa::Juba 2.60
- DateTime::TimeZone::Africa::Khartoum 2.60
- DateTime::TimeZone::Africa::Lagos 2.60
- DateTime::TimeZone::Africa::Maputo 2.60
- DateTime::TimeZone::Africa::Monrovia 2.60
- DateTime::TimeZone::Africa::Nairobi 2.60
- DateTime::TimeZone::Africa::Ndjamena 2.60
- DateTime::TimeZone::Africa::Sao_Tome 2.60
- DateTime::TimeZone::Africa::Tripoli 2.60
- DateTime::TimeZone::Africa::Tunis 2.60
- DateTime::TimeZone::Africa::Windhoek 2.60
- DateTime::TimeZone::America::Adak 2.60
- DateTime::TimeZone::America::Anchorage 2.60
- DateTime::TimeZone::America::Araguaina 2.60
- DateTime::TimeZone::America::Argentina::Buenos_Aires 2.60
- DateTime::TimeZone::America::Argentina::Catamarca 2.60
- DateTime::TimeZone::America::Argentina::Cordoba 2.60
- DateTime::TimeZone::America::Argentina::Jujuy 2.60
- DateTime::TimeZone::America::Argentina::La_Rioja 2.60
- DateTime::TimeZone::America::Argentina::Mendoza 2.60
- DateTime::TimeZone::America::Argentina::Rio_Gallegos 2.60
- DateTime::TimeZone::America::Argentina::Salta 2.60
- DateTime::TimeZone::America::Argentina::San_Juan 2.60
- DateTime::TimeZone::America::Argentina::San_Luis 2.60
- DateTime::TimeZone::America::Argentina::Tucuman 2.60
- DateTime::TimeZone::America::Argentina::Ushuaia 2.60
- DateTime::TimeZone::America::Asuncion 2.60
- DateTime::TimeZone::America::Bahia 2.60
- DateTime::TimeZone::America::Bahia_Banderas 2.60
- DateTime::TimeZone::America::Barbados 2.60
- DateTime::TimeZone::America::Belem 2.60
- DateTime::TimeZone::America::Belize 2.60
- DateTime::TimeZone::America::Boa_Vista 2.60
- DateTime::TimeZone::America::Bogota 2.60
- DateTime::TimeZone::America::Boise 2.60
- DateTime::TimeZone::America::Cambridge_Bay 2.60
- DateTime::TimeZone::America::Campo_Grande 2.60
- DateTime::TimeZone::America::Cancun 2.60
- DateTime::TimeZone::America::Caracas 2.60
- DateTime::TimeZone::America::Cayenne 2.60
- DateTime::TimeZone::America::Chicago 2.60
- DateTime::TimeZone::America::Chihuahua 2.60
- DateTime::TimeZone::America::Ciudad_Juarez 2.60
- DateTime::TimeZone::America::Costa_Rica 2.60
- DateTime::TimeZone::America::Cuiaba 2.60
- DateTime::TimeZone::America::Danmarkshavn 2.60
- DateTime::TimeZone::America::Dawson 2.60
- DateTime::TimeZone::America::Dawson_Creek 2.60
- DateTime::TimeZone::America::Denver 2.60
- DateTime::TimeZone::America::Detroit 2.60
- DateTime::TimeZone::America::Edmonton 2.60
- DateTime::TimeZone::America::Eirunepe 2.60
- DateTime::TimeZone::America::El_Salvador 2.60
- DateTime::TimeZone::America::Fort_Nelson 2.60
- DateTime::TimeZone::America::Fortaleza 2.60
- DateTime::TimeZone::America::Glace_Bay 2.60
- DateTime::TimeZone::America::Goose_Bay 2.60
- DateTime::TimeZone::America::Grand_Turk 2.60
- DateTime::TimeZone::America::Guatemala 2.60
- DateTime::TimeZone::America::Guayaquil 2.60
- DateTime::TimeZone::America::Guyana 2.60
- DateTime::TimeZone::America::Halifax 2.60
- DateTime::TimeZone::America::Havana 2.60
- DateTime::TimeZone::America::Hermosillo 2.60
- DateTime::TimeZone::America::Indiana::Indianapolis 2.60
- DateTime::TimeZone::America::Indiana::Knox 2.60
- DateTime::TimeZone::America::Indiana::Marengo 2.60
- DateTime::TimeZone::America::Indiana::Petersburg 2.60
- DateTime::TimeZone::America::Indiana::Tell_City 2.60
- DateTime::TimeZone::America::Indiana::Vevay 2.60
- DateTime::TimeZone::America::Indiana::Vincennes 2.60
- DateTime::TimeZone::America::Indiana::Winamac 2.60
- DateTime::TimeZone::America::Inuvik 2.60
- DateTime::TimeZone::America::Iqaluit 2.60
- DateTime::TimeZone::America::Jamaica 2.60
- DateTime::TimeZone::America::Juneau 2.60
- DateTime::TimeZone::America::Kentucky::Louisville 2.60
- DateTime::TimeZone::America::Kentucky::Monticello 2.60
- DateTime::TimeZone::America::La_Paz 2.60
- DateTime::TimeZone::America::Lima 2.60
- DateTime::TimeZone::America::Los_Angeles 2.60
- DateTime::TimeZone::America::Maceio 2.60
- DateTime::TimeZone::America::Managua 2.60
- DateTime::TimeZone::America::Manaus 2.60
- DateTime::TimeZone::America::Martinique 2.60
- DateTime::TimeZone::America::Matamoros 2.60
- DateTime::TimeZone::America::Mazatlan 2.60
- DateTime::TimeZone::America::Menominee 2.60
- DateTime::TimeZone::America::Merida 2.60
- DateTime::TimeZone::America::Metlakatla 2.60
- DateTime::TimeZone::America::Mexico_City 2.60
- DateTime::TimeZone::America::Miquelon 2.60
- DateTime::TimeZone::America::Moncton 2.60
- DateTime::TimeZone::America::Monterrey 2.60
- DateTime::TimeZone::America::Montevideo 2.60
- DateTime::TimeZone::America::New_York 2.60
- DateTime::TimeZone::America::Nome 2.60
- DateTime::TimeZone::America::Noronha 2.60
- DateTime::TimeZone::America::North_Dakota::Beulah 2.60
- DateTime::TimeZone::America::North_Dakota::Center 2.60
- DateTime::TimeZone::America::North_Dakota::New_Salem 2.60
- DateTime::TimeZone::America::Nuuk 2.60
- DateTime::TimeZone::America::Ojinaga 2.60
- DateTime::TimeZone::America::Panama 2.60
- DateTime::TimeZone::America::Paramaribo 2.60
- DateTime::TimeZone::America::Phoenix 2.60
- DateTime::TimeZone::America::Port_au_Prince 2.60
- DateTime::TimeZone::America::Porto_Velho 2.60
- DateTime::TimeZone::America::Puerto_Rico 2.60
- DateTime::TimeZone::America::Punta_Arenas 2.60
- DateTime::TimeZone::America::Rankin_Inlet 2.60
- DateTime::TimeZone::America::Recife 2.60
- DateTime::TimeZone::America::Regina 2.60
- DateTime::TimeZone::America::Resolute 2.60
- DateTime::TimeZone::America::Rio_Branco 2.60
- DateTime::TimeZone::America::Santarem 2.60
- DateTime::TimeZone::America::Santiago 2.60
- DateTime::TimeZone::America::Santo_Domingo 2.60
- DateTime::TimeZone::America::Sao_Paulo 2.60
- DateTime::TimeZone::America::Scoresbysund 2.60
- DateTime::TimeZone::America::Sitka 2.60
- DateTime::TimeZone::America::St_Johns 2.60
- DateTime::TimeZone::America::Swift_Current 2.60
- DateTime::TimeZone::America::Tegucigalpa 2.60
- DateTime::TimeZone::America::Thule 2.60
- DateTime::TimeZone::America::Tijuana 2.60
- DateTime::TimeZone::America::Toronto 2.60
- DateTime::TimeZone::America::Vancouver 2.60
- DateTime::TimeZone::America::Whitehorse 2.60
- DateTime::TimeZone::America::Winnipeg 2.60
- DateTime::TimeZone::America::Yakutat 2.60
- DateTime::TimeZone::Antarctica::Casey 2.60
- DateTime::TimeZone::Antarctica::Davis 2.60
- DateTime::TimeZone::Antarctica::Macquarie 2.60
- DateTime::TimeZone::Antarctica::Mawson 2.60
- DateTime::TimeZone::Antarctica::Palmer 2.60
- DateTime::TimeZone::Antarctica::Rothera 2.60
- DateTime::TimeZone::Antarctica::Troll 2.60
- DateTime::TimeZone::Asia::Almaty 2.60
- DateTime::TimeZone::Asia::Amman 2.60
- DateTime::TimeZone::Asia::Anadyr 2.60
- DateTime::TimeZone::Asia::Aqtau 2.60
- DateTime::TimeZone::Asia::Aqtobe 2.60
- DateTime::TimeZone::Asia::Ashgabat 2.60
- DateTime::TimeZone::Asia::Atyrau 2.60
- DateTime::TimeZone::Asia::Baghdad 2.60
- DateTime::TimeZone::Asia::Baku 2.60
- DateTime::TimeZone::Asia::Bangkok 2.60
- DateTime::TimeZone::Asia::Barnaul 2.60
- DateTime::TimeZone::Asia::Beirut 2.60
- DateTime::TimeZone::Asia::Bishkek 2.60
- DateTime::TimeZone::Asia::Chita 2.60
- DateTime::TimeZone::Asia::Choibalsan 2.60
- DateTime::TimeZone::Asia::Colombo 2.60
- DateTime::TimeZone::Asia::Damascus 2.60
- DateTime::TimeZone::Asia::Dhaka 2.60
- DateTime::TimeZone::Asia::Dili 2.60
- DateTime::TimeZone::Asia::Dubai 2.60
- DateTime::TimeZone::Asia::Dushanbe 2.60
- DateTime::TimeZone::Asia::Famagusta 2.60
- DateTime::TimeZone::Asia::Gaza 2.60
- DateTime::TimeZone::Asia::Hebron 2.60
- DateTime::TimeZone::Asia::Ho_Chi_Minh 2.60
- DateTime::TimeZone::Asia::Hong_Kong 2.60
- DateTime::TimeZone::Asia::Hovd 2.60
- DateTime::TimeZone::Asia::Irkutsk 2.60
- DateTime::TimeZone::Asia::Jakarta 2.60
- DateTime::TimeZone::Asia::Jayapura 2.60
- DateTime::TimeZone::Asia::Jerusalem 2.60
- DateTime::TimeZone::Asia::Kabul 2.60
- DateTime::TimeZone::Asia::Kamchatka 2.60
- DateTime::TimeZone::Asia::Karachi 2.60
- DateTime::TimeZone::Asia::Kathmandu 2.60
- DateTime::TimeZone::Asia::Khandyga 2.60
- DateTime::TimeZone::Asia::Kolkata 2.60
- DateTime::TimeZone::Asia::Krasnoyarsk 2.60
- DateTime::TimeZone::Asia::Kuching 2.60
- DateTime::TimeZone::Asia::Macau 2.60
- DateTime::TimeZone::Asia::Magadan 2.60
- DateTime::TimeZone::Asia::Makassar 2.60
- DateTime::TimeZone::Asia::Manila 2.60
- DateTime::TimeZone::Asia::Nicosia 2.60
- DateTime::TimeZone::Asia::Novokuznetsk 2.60
- DateTime::TimeZone::Asia::Novosibirsk 2.60
- DateTime::TimeZone::Asia::Omsk 2.60
- DateTime::TimeZone::Asia::Oral 2.60
- DateTime::TimeZone::Asia::Pontianak 2.60
- DateTime::TimeZone::Asia::Pyongyang 2.60
- DateTime::TimeZone::Asia::Qatar 2.60
- DateTime::TimeZone::Asia::Qostanay 2.60
- DateTime::TimeZone::Asia::Qyzylorda 2.60
- DateTime::TimeZone::Asia::Riyadh 2.60
- DateTime::TimeZone::Asia::Sakhalin 2.60
- DateTime::TimeZone::Asia::Samarkand 2.60
- DateTime::TimeZone::Asia::Seoul 2.60
- DateTime::TimeZone::Asia::Shanghai 2.60
- DateTime::TimeZone::Asia::Singapore 2.60
- DateTime::TimeZone::Asia::Srednekolymsk 2.60
- DateTime::TimeZone::Asia::Taipei 2.60
- DateTime::TimeZone::Asia::Tashkent 2.60
- DateTime::TimeZone::Asia::Tbilisi 2.60
- DateTime::TimeZone::Asia::Tehran 2.60
- DateTime::TimeZone::Asia::Thimphu 2.60
- DateTime::TimeZone::Asia::Tokyo 2.60
- DateTime::TimeZone::Asia::Tomsk 2.60
- DateTime::TimeZone::Asia::Ulaanbaatar 2.60
- DateTime::TimeZone::Asia::Urumqi 2.60
- DateTime::TimeZone::Asia::Ust_Nera 2.60
- DateTime::TimeZone::Asia::Vladivostok 2.60
- DateTime::TimeZone::Asia::Yakutsk 2.60
- DateTime::TimeZone::Asia::Yangon 2.60
- DateTime::TimeZone::Asia::Yekaterinburg 2.60
- DateTime::TimeZone::Asia::Yerevan 2.60
- DateTime::TimeZone::Atlantic::Azores 2.60
- DateTime::TimeZone::Atlantic::Bermuda 2.60
- DateTime::TimeZone::Atlantic::Canary 2.60
- DateTime::TimeZone::Atlantic::Cape_Verde 2.60
- DateTime::TimeZone::Atlantic::Faroe 2.60
- DateTime::TimeZone::Atlantic::Madeira 2.60
- DateTime::TimeZone::Atlantic::South_Georgia 2.60
- DateTime::TimeZone::Atlantic::Stanley 2.60
- DateTime::TimeZone::Australia::Adelaide 2.60
- DateTime::TimeZone::Australia::Brisbane 2.60
- DateTime::TimeZone::Australia::Broken_Hill 2.60
- DateTime::TimeZone::Australia::Darwin 2.60
- DateTime::TimeZone::Australia::Eucla 2.60
- DateTime::TimeZone::Australia::Hobart 2.60
- DateTime::TimeZone::Australia::Lindeman 2.60
- DateTime::TimeZone::Australia::Lord_Howe 2.60
- DateTime::TimeZone::Australia::Melbourne 2.60
- DateTime::TimeZone::Australia::Perth 2.60
- DateTime::TimeZone::Australia::Sydney 2.60
- DateTime::TimeZone::CET 2.60
- DateTime::TimeZone::CST6CDT 2.60
- DateTime::TimeZone::Catalog 2.60
- DateTime::TimeZone::EET 2.60
- DateTime::TimeZone::EST 2.60
- DateTime::TimeZone::EST5EDT 2.60
- DateTime::TimeZone::Europe::Andorra 2.60
- DateTime::TimeZone::Europe::Astrakhan 2.60
- DateTime::TimeZone::Europe::Athens 2.60
- DateTime::TimeZone::Europe::Belgrade 2.60
- DateTime::TimeZone::Europe::Berlin 2.60
- DateTime::TimeZone::Europe::Brussels 2.60
- DateTime::TimeZone::Europe::Bucharest 2.60
- DateTime::TimeZone::Europe::Budapest 2.60
- DateTime::TimeZone::Europe::Chisinau 2.60
- DateTime::TimeZone::Europe::Dublin 2.60
- DateTime::TimeZone::Europe::Gibraltar 2.60
- DateTime::TimeZone::Europe::Helsinki 2.60
- DateTime::TimeZone::Europe::Istanbul 2.60
- DateTime::TimeZone::Europe::Kaliningrad 2.60
- DateTime::TimeZone::Europe::Kirov 2.60
- DateTime::TimeZone::Europe::Kyiv 2.60
- DateTime::TimeZone::Europe::Lisbon 2.60
- DateTime::TimeZone::Europe::London 2.60
- DateTime::TimeZone::Europe::Madrid 2.60
- DateTime::TimeZone::Europe::Malta 2.60
- DateTime::TimeZone::Europe::Minsk 2.60
- DateTime::TimeZone::Europe::Moscow 2.60
- DateTime::TimeZone::Europe::Paris 2.60
- DateTime::TimeZone::Europe::Prague 2.60
- DateTime::TimeZone::Europe::Riga 2.60
- DateTime::TimeZone::Europe::Rome 2.60
- DateTime::TimeZone::Europe::Samara 2.60
- DateTime::TimeZone::Europe::Saratov 2.60
- DateTime::TimeZone::Europe::Simferopol 2.60
- DateTime::TimeZone::Europe::Sofia 2.60
- DateTime::TimeZone::Europe::Tallinn 2.60
- DateTime::TimeZone::Europe::Tirane 2.60
- DateTime::TimeZone::Europe::Ulyanovsk 2.60
- DateTime::TimeZone::Europe::Vienna 2.60
- DateTime::TimeZone::Europe::Vilnius 2.60
- DateTime::TimeZone::Europe::Volgograd 2.60
- DateTime::TimeZone::Europe::Warsaw 2.60
- DateTime::TimeZone::Europe::Zurich 2.60
- DateTime::TimeZone::Floating 2.60
- DateTime::TimeZone::HST 2.60
- DateTime::TimeZone::Indian::Chagos 2.60
- DateTime::TimeZone::Indian::Maldives 2.60
- DateTime::TimeZone::Indian::Mauritius 2.60
- DateTime::TimeZone::Local 2.60
- DateTime::TimeZone::Local::Android 2.60
- DateTime::TimeZone::Local::Unix 2.60
- DateTime::TimeZone::Local::VMS 2.60
- DateTime::TimeZone::MET 2.60
- DateTime::TimeZone::MST 2.60
- DateTime::TimeZone::MST7MDT 2.60
- DateTime::TimeZone::OffsetOnly 2.60
- DateTime::TimeZone::OlsonDB 2.60
- DateTime::TimeZone::OlsonDB::Change 2.60
- DateTime::TimeZone::OlsonDB::Observance 2.60
- DateTime::TimeZone::OlsonDB::Rule 2.60
- DateTime::TimeZone::OlsonDB::Zone 2.60
- DateTime::TimeZone::PST8PDT 2.60
- DateTime::TimeZone::Pacific::Apia 2.60
- DateTime::TimeZone::Pacific::Auckland 2.60
- DateTime::TimeZone::Pacific::Bougainville 2.60
- DateTime::TimeZone::Pacific::Chatham 2.60
- DateTime::TimeZone::Pacific::Easter 2.60
- DateTime::TimeZone::Pacific::Efate 2.60
- DateTime::TimeZone::Pacific::Fakaofo 2.60
- DateTime::TimeZone::Pacific::Fiji 2.60
- DateTime::TimeZone::Pacific::Galapagos 2.60
- DateTime::TimeZone::Pacific::Gambier 2.60
- DateTime::TimeZone::Pacific::Guadalcanal 2.60
- DateTime::TimeZone::Pacific::Guam 2.60
- DateTime::TimeZone::Pacific::Honolulu 2.60
- DateTime::TimeZone::Pacific::Kanton 2.60
- DateTime::TimeZone::Pacific::Kiritimati 2.60
- DateTime::TimeZone::Pacific::Kosrae 2.60
- DateTime::TimeZone::Pacific::Kwajalein 2.60
- DateTime::TimeZone::Pacific::Marquesas 2.60
- DateTime::TimeZone::Pacific::Nauru 2.60
- DateTime::TimeZone::Pacific::Niue 2.60
- DateTime::TimeZone::Pacific::Norfolk 2.60
- DateTime::TimeZone::Pacific::Noumea 2.60
- DateTime::TimeZone::Pacific::Pago_Pago 2.60
- DateTime::TimeZone::Pacific::Palau 2.60
- DateTime::TimeZone::Pacific::Pitcairn 2.60
- DateTime::TimeZone::Pacific::Port_Moresby 2.60
- DateTime::TimeZone::Pacific::Rarotonga 2.60
- DateTime::TimeZone::Pacific::Tahiti 2.60
- DateTime::TimeZone::Pacific::Tarawa 2.60
- DateTime::TimeZone::Pacific::Tongatapu 2.60
- DateTime::TimeZone::UTC 2.60
- DateTime::TimeZone::WET 2.60
+ DateTime-TimeZone-2.65
+ pathname: D/DR/DROLSKY/DateTime-TimeZone-2.65.tar.gz
+ provides:
+ DateTime::TimeZone 2.65
+ DateTime::TimeZone::Africa::Abidjan 2.65
+ DateTime::TimeZone::Africa::Algiers 2.65
+ DateTime::TimeZone::Africa::Bissau 2.65
+ DateTime::TimeZone::Africa::Cairo 2.65
+ DateTime::TimeZone::Africa::Casablanca 2.65
+ DateTime::TimeZone::Africa::Ceuta 2.65
+ DateTime::TimeZone::Africa::El_Aaiun 2.65
+ DateTime::TimeZone::Africa::Johannesburg 2.65
+ DateTime::TimeZone::Africa::Juba 2.65
+ DateTime::TimeZone::Africa::Khartoum 2.65
+ DateTime::TimeZone::Africa::Lagos 2.65
+ DateTime::TimeZone::Africa::Maputo 2.65
+ DateTime::TimeZone::Africa::Monrovia 2.65
+ DateTime::TimeZone::Africa::Nairobi 2.65
+ DateTime::TimeZone::Africa::Ndjamena 2.65
+ DateTime::TimeZone::Africa::Sao_Tome 2.65
+ DateTime::TimeZone::Africa::Tripoli 2.65
+ DateTime::TimeZone::Africa::Tunis 2.65
+ DateTime::TimeZone::Africa::Windhoek 2.65
+ DateTime::TimeZone::America::Adak 2.65
+ DateTime::TimeZone::America::Anchorage 2.65
+ DateTime::TimeZone::America::Araguaina 2.65
+ DateTime::TimeZone::America::Argentina::Buenos_Aires 2.65
+ DateTime::TimeZone::America::Argentina::Catamarca 2.65
+ DateTime::TimeZone::America::Argentina::Cordoba 2.65
+ DateTime::TimeZone::America::Argentina::Jujuy 2.65
+ DateTime::TimeZone::America::Argentina::La_Rioja 2.65
+ DateTime::TimeZone::America::Argentina::Mendoza 2.65
+ DateTime::TimeZone::America::Argentina::Rio_Gallegos 2.65
+ DateTime::TimeZone::America::Argentina::Salta 2.65
+ DateTime::TimeZone::America::Argentina::San_Juan 2.65
+ DateTime::TimeZone::America::Argentina::San_Luis 2.65
+ DateTime::TimeZone::America::Argentina::Tucuman 2.65
+ DateTime::TimeZone::America::Argentina::Ushuaia 2.65
+ DateTime::TimeZone::America::Asuncion 2.65
+ DateTime::TimeZone::America::Bahia 2.65
+ DateTime::TimeZone::America::Bahia_Banderas 2.65
+ DateTime::TimeZone::America::Barbados 2.65
+ DateTime::TimeZone::America::Belem 2.65
+ DateTime::TimeZone::America::Belize 2.65
+ DateTime::TimeZone::America::Boa_Vista 2.65
+ DateTime::TimeZone::America::Bogota 2.65
+ DateTime::TimeZone::America::Boise 2.65
+ DateTime::TimeZone::America::Cambridge_Bay 2.65
+ DateTime::TimeZone::America::Campo_Grande 2.65
+ DateTime::TimeZone::America::Cancun 2.65
+ DateTime::TimeZone::America::Caracas 2.65
+ DateTime::TimeZone::America::Cayenne 2.65
+ DateTime::TimeZone::America::Chicago 2.65
+ DateTime::TimeZone::America::Chihuahua 2.65
+ DateTime::TimeZone::America::Ciudad_Juarez 2.65
+ DateTime::TimeZone::America::Costa_Rica 2.65
+ DateTime::TimeZone::America::Coyhaique 2.65
+ DateTime::TimeZone::America::Cuiaba 2.65
+ DateTime::TimeZone::America::Danmarkshavn 2.65
+ DateTime::TimeZone::America::Dawson 2.65
+ DateTime::TimeZone::America::Dawson_Creek 2.65
+ DateTime::TimeZone::America::Denver 2.65
+ DateTime::TimeZone::America::Detroit 2.65
+ DateTime::TimeZone::America::Edmonton 2.65
+ DateTime::TimeZone::America::Eirunepe 2.65
+ DateTime::TimeZone::America::El_Salvador 2.65
+ DateTime::TimeZone::America::Fort_Nelson 2.65
+ DateTime::TimeZone::America::Fortaleza 2.65
+ DateTime::TimeZone::America::Glace_Bay 2.65
+ DateTime::TimeZone::America::Goose_Bay 2.65
+ DateTime::TimeZone::America::Grand_Turk 2.65
+ DateTime::TimeZone::America::Guatemala 2.65
+ DateTime::TimeZone::America::Guayaquil 2.65
+ DateTime::TimeZone::America::Guyana 2.65
+ DateTime::TimeZone::America::Halifax 2.65
+ DateTime::TimeZone::America::Havana 2.65
+ DateTime::TimeZone::America::Hermosillo 2.65
+ DateTime::TimeZone::America::Indiana::Indianapolis 2.65
+ DateTime::TimeZone::America::Indiana::Knox 2.65
+ DateTime::TimeZone::America::Indiana::Marengo 2.65
+ DateTime::TimeZone::America::Indiana::Petersburg 2.65
+ DateTime::TimeZone::America::Indiana::Tell_City 2.65
+ DateTime::TimeZone::America::Indiana::Vevay 2.65
+ DateTime::TimeZone::America::Indiana::Vincennes 2.65
+ DateTime::TimeZone::America::Indiana::Winamac 2.65
+ DateTime::TimeZone::America::Inuvik 2.65
+ DateTime::TimeZone::America::Iqaluit 2.65
+ DateTime::TimeZone::America::Jamaica 2.65
+ DateTime::TimeZone::America::Juneau 2.65
+ DateTime::TimeZone::America::Kentucky::Louisville 2.65
+ DateTime::TimeZone::America::Kentucky::Monticello 2.65
+ DateTime::TimeZone::America::La_Paz 2.65
+ DateTime::TimeZone::America::Lima 2.65
+ DateTime::TimeZone::America::Los_Angeles 2.65
+ DateTime::TimeZone::America::Maceio 2.65
+ DateTime::TimeZone::America::Managua 2.65
+ DateTime::TimeZone::America::Manaus 2.65
+ DateTime::TimeZone::America::Martinique 2.65
+ DateTime::TimeZone::America::Matamoros 2.65
+ DateTime::TimeZone::America::Mazatlan 2.65
+ DateTime::TimeZone::America::Menominee 2.65
+ DateTime::TimeZone::America::Merida 2.65
+ DateTime::TimeZone::America::Metlakatla 2.65
+ DateTime::TimeZone::America::Mexico_City 2.65
+ DateTime::TimeZone::America::Miquelon 2.65
+ DateTime::TimeZone::America::Moncton 2.65
+ DateTime::TimeZone::America::Monterrey 2.65
+ DateTime::TimeZone::America::Montevideo 2.65
+ DateTime::TimeZone::America::New_York 2.65
+ DateTime::TimeZone::America::Nome 2.65
+ DateTime::TimeZone::America::Noronha 2.65
+ DateTime::TimeZone::America::North_Dakota::Beulah 2.65
+ DateTime::TimeZone::America::North_Dakota::Center 2.65
+ DateTime::TimeZone::America::North_Dakota::New_Salem 2.65
+ DateTime::TimeZone::America::Nuuk 2.65
+ DateTime::TimeZone::America::Ojinaga 2.65
+ DateTime::TimeZone::America::Panama 2.65
+ DateTime::TimeZone::America::Paramaribo 2.65
+ DateTime::TimeZone::America::Phoenix 2.65
+ DateTime::TimeZone::America::Port_au_Prince 2.65
+ DateTime::TimeZone::America::Porto_Velho 2.65
+ DateTime::TimeZone::America::Puerto_Rico 2.65
+ DateTime::TimeZone::America::Punta_Arenas 2.65
+ DateTime::TimeZone::America::Rankin_Inlet 2.65
+ DateTime::TimeZone::America::Recife 2.65
+ DateTime::TimeZone::America::Regina 2.65
+ DateTime::TimeZone::America::Resolute 2.65
+ DateTime::TimeZone::America::Rio_Branco 2.65
+ DateTime::TimeZone::America::Santarem 2.65
+ DateTime::TimeZone::America::Santiago 2.65
+ DateTime::TimeZone::America::Santo_Domingo 2.65
+ DateTime::TimeZone::America::Sao_Paulo 2.65
+ DateTime::TimeZone::America::Scoresbysund 2.65
+ DateTime::TimeZone::America::Sitka 2.65
+ DateTime::TimeZone::America::St_Johns 2.65
+ DateTime::TimeZone::America::Swift_Current 2.65
+ DateTime::TimeZone::America::Tegucigalpa 2.65
+ DateTime::TimeZone::America::Thule 2.65
+ DateTime::TimeZone::America::Tijuana 2.65
+ DateTime::TimeZone::America::Toronto 2.65
+ DateTime::TimeZone::America::Vancouver 2.65
+ DateTime::TimeZone::America::Whitehorse 2.65
+ DateTime::TimeZone::America::Winnipeg 2.65
+ DateTime::TimeZone::America::Yakutat 2.65
+ DateTime::TimeZone::Antarctica::Casey 2.65
+ DateTime::TimeZone::Antarctica::Davis 2.65
+ DateTime::TimeZone::Antarctica::Macquarie 2.65
+ DateTime::TimeZone::Antarctica::Mawson 2.65
+ DateTime::TimeZone::Antarctica::Palmer 2.65
+ DateTime::TimeZone::Antarctica::Rothera 2.65
+ DateTime::TimeZone::Antarctica::Troll 2.65
+ DateTime::TimeZone::Antarctica::Vostok 2.65
+ DateTime::TimeZone::Asia::Almaty 2.65
+ DateTime::TimeZone::Asia::Amman 2.65
+ DateTime::TimeZone::Asia::Anadyr 2.65
+ DateTime::TimeZone::Asia::Aqtau 2.65
+ DateTime::TimeZone::Asia::Aqtobe 2.65
+ DateTime::TimeZone::Asia::Ashgabat 2.65
+ DateTime::TimeZone::Asia::Atyrau 2.65
+ DateTime::TimeZone::Asia::Baghdad 2.65
+ DateTime::TimeZone::Asia::Baku 2.65
+ DateTime::TimeZone::Asia::Bangkok 2.65
+ DateTime::TimeZone::Asia::Barnaul 2.65
+ DateTime::TimeZone::Asia::Beirut 2.65
+ DateTime::TimeZone::Asia::Bishkek 2.65
+ DateTime::TimeZone::Asia::Chita 2.65
+ DateTime::TimeZone::Asia::Colombo 2.65
+ DateTime::TimeZone::Asia::Damascus 2.65
+ DateTime::TimeZone::Asia::Dhaka 2.65
+ DateTime::TimeZone::Asia::Dili 2.65
+ DateTime::TimeZone::Asia::Dubai 2.65
+ DateTime::TimeZone::Asia::Dushanbe 2.65
+ DateTime::TimeZone::Asia::Famagusta 2.65
+ DateTime::TimeZone::Asia::Gaza 2.65
+ DateTime::TimeZone::Asia::Hebron 2.65
+ DateTime::TimeZone::Asia::Ho_Chi_Minh 2.65
+ DateTime::TimeZone::Asia::Hong_Kong 2.65
+ DateTime::TimeZone::Asia::Hovd 2.65
+ DateTime::TimeZone::Asia::Irkutsk 2.65
+ DateTime::TimeZone::Asia::Jakarta 2.65
+ DateTime::TimeZone::Asia::Jayapura 2.65
+ DateTime::TimeZone::Asia::Jerusalem 2.65
+ DateTime::TimeZone::Asia::Kabul 2.65
+ DateTime::TimeZone::Asia::Kamchatka 2.65
+ DateTime::TimeZone::Asia::Karachi 2.65
+ DateTime::TimeZone::Asia::Kathmandu 2.65
+ DateTime::TimeZone::Asia::Khandyga 2.65
+ DateTime::TimeZone::Asia::Kolkata 2.65
+ DateTime::TimeZone::Asia::Krasnoyarsk 2.65
+ DateTime::TimeZone::Asia::Kuching 2.65
+ DateTime::TimeZone::Asia::Macau 2.65
+ DateTime::TimeZone::Asia::Magadan 2.65
+ DateTime::TimeZone::Asia::Makassar 2.65
+ DateTime::TimeZone::Asia::Manila 2.65
+ DateTime::TimeZone::Asia::Nicosia 2.65
+ DateTime::TimeZone::Asia::Novokuznetsk 2.65
+ DateTime::TimeZone::Asia::Novosibirsk 2.65
+ DateTime::TimeZone::Asia::Omsk 2.65
+ DateTime::TimeZone::Asia::Oral 2.65
+ DateTime::TimeZone::Asia::Pontianak 2.65
+ DateTime::TimeZone::Asia::Pyongyang 2.65
+ DateTime::TimeZone::Asia::Qatar 2.65
+ DateTime::TimeZone::Asia::Qostanay 2.65
+ DateTime::TimeZone::Asia::Qyzylorda 2.65
+ DateTime::TimeZone::Asia::Riyadh 2.65
+ DateTime::TimeZone::Asia::Sakhalin 2.65
+ DateTime::TimeZone::Asia::Samarkand 2.65
+ DateTime::TimeZone::Asia::Seoul 2.65
+ DateTime::TimeZone::Asia::Shanghai 2.65
+ DateTime::TimeZone::Asia::Singapore 2.65
+ DateTime::TimeZone::Asia::Srednekolymsk 2.65
+ DateTime::TimeZone::Asia::Taipei 2.65
+ DateTime::TimeZone::Asia::Tashkent 2.65
+ DateTime::TimeZone::Asia::Tbilisi 2.65
+ DateTime::TimeZone::Asia::Tehran 2.65
+ DateTime::TimeZone::Asia::Thimphu 2.65
+ DateTime::TimeZone::Asia::Tokyo 2.65
+ DateTime::TimeZone::Asia::Tomsk 2.65
+ DateTime::TimeZone::Asia::Ulaanbaatar 2.65
+ DateTime::TimeZone::Asia::Urumqi 2.65
+ DateTime::TimeZone::Asia::Ust_Nera 2.65
+ DateTime::TimeZone::Asia::Vladivostok 2.65
+ DateTime::TimeZone::Asia::Yakutsk 2.65
+ DateTime::TimeZone::Asia::Yangon 2.65
+ DateTime::TimeZone::Asia::Yekaterinburg 2.65
+ DateTime::TimeZone::Asia::Yerevan 2.65
+ DateTime::TimeZone::Atlantic::Azores 2.65
+ DateTime::TimeZone::Atlantic::Bermuda 2.65
+ DateTime::TimeZone::Atlantic::Canary 2.65
+ DateTime::TimeZone::Atlantic::Cape_Verde 2.65
+ DateTime::TimeZone::Atlantic::Faroe 2.65
+ DateTime::TimeZone::Atlantic::Madeira 2.65
+ DateTime::TimeZone::Atlantic::South_Georgia 2.65
+ DateTime::TimeZone::Atlantic::Stanley 2.65
+ DateTime::TimeZone::Australia::Adelaide 2.65
+ DateTime::TimeZone::Australia::Brisbane 2.65
+ DateTime::TimeZone::Australia::Broken_Hill 2.65
+ DateTime::TimeZone::Australia::Darwin 2.65
+ DateTime::TimeZone::Australia::Eucla 2.65
+ DateTime::TimeZone::Australia::Hobart 2.65
+ DateTime::TimeZone::Australia::Lindeman 2.65
+ DateTime::TimeZone::Australia::Lord_Howe 2.65
+ DateTime::TimeZone::Australia::Melbourne 2.65
+ DateTime::TimeZone::Australia::Perth 2.65
+ DateTime::TimeZone::Australia::Sydney 2.65
+ DateTime::TimeZone::Catalog 2.65
+ DateTime::TimeZone::Europe::Andorra 2.65
+ DateTime::TimeZone::Europe::Astrakhan 2.65
+ DateTime::TimeZone::Europe::Athens 2.65
+ DateTime::TimeZone::Europe::Belgrade 2.65
+ DateTime::TimeZone::Europe::Berlin 2.65
+ DateTime::TimeZone::Europe::Brussels 2.65
+ DateTime::TimeZone::Europe::Bucharest 2.65
+ DateTime::TimeZone::Europe::Budapest 2.65
+ DateTime::TimeZone::Europe::Chisinau 2.65
+ DateTime::TimeZone::Europe::Dublin 2.65
+ DateTime::TimeZone::Europe::Gibraltar 2.65
+ DateTime::TimeZone::Europe::Helsinki 2.65
+ DateTime::TimeZone::Europe::Istanbul 2.65
+ DateTime::TimeZone::Europe::Kaliningrad 2.65
+ DateTime::TimeZone::Europe::Kirov 2.65
+ DateTime::TimeZone::Europe::Kyiv 2.65
+ DateTime::TimeZone::Europe::Lisbon 2.65
+ DateTime::TimeZone::Europe::London 2.65
+ DateTime::TimeZone::Europe::Madrid 2.65
+ DateTime::TimeZone::Europe::Malta 2.65
+ DateTime::TimeZone::Europe::Minsk 2.65
+ DateTime::TimeZone::Europe::Moscow 2.65
+ DateTime::TimeZone::Europe::Paris 2.65
+ DateTime::TimeZone::Europe::Prague 2.65
+ DateTime::TimeZone::Europe::Riga 2.65
+ DateTime::TimeZone::Europe::Rome 2.65
+ DateTime::TimeZone::Europe::Samara 2.65
+ DateTime::TimeZone::Europe::Saratov 2.65
+ DateTime::TimeZone::Europe::Simferopol 2.65
+ DateTime::TimeZone::Europe::Sofia 2.65
+ DateTime::TimeZone::Europe::Tallinn 2.65
+ DateTime::TimeZone::Europe::Tirane 2.65
+ DateTime::TimeZone::Europe::Ulyanovsk 2.65
+ DateTime::TimeZone::Europe::Vienna 2.65
+ DateTime::TimeZone::Europe::Vilnius 2.65
+ DateTime::TimeZone::Europe::Volgograd 2.65
+ DateTime::TimeZone::Europe::Warsaw 2.65
+ DateTime::TimeZone::Europe::Zurich 2.65
+ DateTime::TimeZone::Floating 2.65
+ DateTime::TimeZone::Indian::Chagos 2.65
+ DateTime::TimeZone::Indian::Maldives 2.65
+ DateTime::TimeZone::Indian::Mauritius 2.65
+ DateTime::TimeZone::Local 2.65
+ DateTime::TimeZone::Local::Android 2.65
+ DateTime::TimeZone::Local::Unix 2.65
+ DateTime::TimeZone::Local::VMS 2.65
+ DateTime::TimeZone::OffsetOnly 2.65
+ DateTime::TimeZone::OlsonDB 2.65
+ DateTime::TimeZone::OlsonDB::Change 2.65
+ DateTime::TimeZone::OlsonDB::Observance 2.65
+ DateTime::TimeZone::OlsonDB::Rule 2.65
+ DateTime::TimeZone::OlsonDB::Zone 2.65
+ DateTime::TimeZone::Pacific::Apia 2.65
+ DateTime::TimeZone::Pacific::Auckland 2.65
+ DateTime::TimeZone::Pacific::Bougainville 2.65
+ DateTime::TimeZone::Pacific::Chatham 2.65
+ DateTime::TimeZone::Pacific::Easter 2.65
+ DateTime::TimeZone::Pacific::Efate 2.65
+ DateTime::TimeZone::Pacific::Fakaofo 2.65
+ DateTime::TimeZone::Pacific::Fiji 2.65
+ DateTime::TimeZone::Pacific::Galapagos 2.65
+ DateTime::TimeZone::Pacific::Gambier 2.65
+ DateTime::TimeZone::Pacific::Guadalcanal 2.65
+ DateTime::TimeZone::Pacific::Guam 2.65
+ DateTime::TimeZone::Pacific::Honolulu 2.65
+ DateTime::TimeZone::Pacific::Kanton 2.65
+ DateTime::TimeZone::Pacific::Kiritimati 2.65
+ DateTime::TimeZone::Pacific::Kosrae 2.65
+ DateTime::TimeZone::Pacific::Kwajalein 2.65
+ DateTime::TimeZone::Pacific::Marquesas 2.65
+ DateTime::TimeZone::Pacific::Nauru 2.65
+ DateTime::TimeZone::Pacific::Niue 2.65
+ DateTime::TimeZone::Pacific::Norfolk 2.65
+ DateTime::TimeZone::Pacific::Noumea 2.65
+ DateTime::TimeZone::Pacific::Pago_Pago 2.65
+ DateTime::TimeZone::Pacific::Palau 2.65
+ DateTime::TimeZone::Pacific::Pitcairn 2.65
+ DateTime::TimeZone::Pacific::Port_Moresby 2.65
+ DateTime::TimeZone::Pacific::Rarotonga 2.65
+ DateTime::TimeZone::Pacific::Tahiti 2.65
+ DateTime::TimeZone::Pacific::Tarawa 2.65
+ DateTime::TimeZone::Pacific::Tongatapu 2.65
+ DateTime::TimeZone::UTC 2.65
requirements:
Class::Singleton 1.03
Cwd 3
@@ -738,11 +779,11 @@ DISTRIBUTIONS
perl 5.008004
strict 0
warnings 0
- Devel-StackTrace-2.04
- pathname: D/DR/DROLSKY/Devel-StackTrace-2.04.tar.gz
+ Devel-StackTrace-2.05
+ pathname: D/DR/DROLSKY/Devel-StackTrace-2.05.tar.gz
provides:
- Devel::StackTrace 2.04
- Devel::StackTrace::Frame 2.04
+ Devel::StackTrace 2.05
+ Devel::StackTrace::Frame 2.05
requirements:
ExtUtils::MakeMaker 0
File::Spec 0
@@ -809,32 +850,35 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 6.17
perl 5.006001
- ExtUtils-Config-0.008
- pathname: L/LE/LEONT/ExtUtils-Config-0.008.tar.gz
+ ExtUtils-Config-0.010
+ pathname: L/LE/LEONT/ExtUtils-Config-0.010.tar.gz
provides:
- ExtUtils::Config 0.008
+ ExtUtils::Config 0.010
+ ExtUtils::Config::MakeMaker 0.010
requirements:
Data::Dumper 0
- ExtUtils::MakeMaker 6.30
+ ExtUtils::MakeMaker 0
+ ExtUtils::MakeMaker::Config 0
+ perl 5.006
strict 0
warnings 0
- ExtUtils-Depends-0.8001
- pathname: X/XA/XAOC/ExtUtils-Depends-0.8001.tar.gz
+ ExtUtils-Depends-0.8002
+ pathname: E/ET/ETJ/ExtUtils-Depends-0.8002.tar.gz
provides:
- ExtUtils::Depends 0.8001
+ ExtUtils::Depends 0.8002
requirements:
Data::Dumper 0
ExtUtils::MakeMaker 7.44
File::Spec 0
IO::File 0
perl 5.006
- ExtUtils-Helpers-0.026
- pathname: L/LE/LEONT/ExtUtils-Helpers-0.026.tar.gz
+ ExtUtils-Helpers-0.028
+ pathname: L/LE/LEONT/ExtUtils-Helpers-0.028.tar.gz
provides:
- ExtUtils::Helpers 0.026
- ExtUtils::Helpers::Unix 0.026
- ExtUtils::Helpers::VMS 0.026
- ExtUtils::Helpers::Windows 0.026
+ ExtUtils::Helpers 0.028
+ ExtUtils::Helpers::Unix 0.028
+ ExtUtils::Helpers::VMS 0.028
+ ExtUtils::Helpers::Windows 0.028
requirements:
Carp 0
Exporter 5.57
@@ -843,19 +887,18 @@ DISTRIBUTIONS
File::Copy 0
File::Spec::Functions 0
Text::ParseWords 3.24
- perl 5.006
strict 0
warnings 0
- ExtUtils-InstallPaths-0.012
- pathname: L/LE/LEONT/ExtUtils-InstallPaths-0.012.tar.gz
+ ExtUtils-InstallPaths-0.014
+ pathname: L/LE/LEONT/ExtUtils-InstallPaths-0.014.tar.gz
provides:
- ExtUtils::InstallPaths 0.012
+ ExtUtils::InstallPaths 0.014
requirements:
Carp 0
- ExtUtils::Config 0.002
+ ExtUtils::Config 0.009
ExtUtils::MakeMaker 0
File::Spec 0
- perl 5.006
+ perl 5.008
strict 0
warnings 0
FFI-CheckLib-0.31
@@ -996,16 +1039,16 @@ DISTRIBUTIONS
parent 0
perl 5.008001
strictures 2.000000
- HTML-Parser-3.81
- pathname: O/OA/OALDERS/HTML-Parser-3.81.tar.gz
+ HTML-Parser-3.83
+ pathname: O/OA/OALDERS/HTML-Parser-3.83.tar.gz
provides:
- HTML::Entities 3.81
- HTML::Filter 3.81
- HTML::HeadParser 3.81
- HTML::LinkExtor 3.81
- HTML::Parser 3.81
- HTML::PullParser 3.81
- HTML::TokeParser 3.81
+ HTML::Entities 3.83
+ HTML::Filter 3.83
+ HTML::HeadParser 3.83
+ HTML::LinkExtor 3.83
+ HTML::Parser 3.83
+ HTML::PullParser 3.83
+ HTML::TokeParser 3.83
requirements:
Carp 0
Exporter 0
@@ -1017,32 +1060,19 @@ DISTRIBUTIONS
URI::URL 0
XSLoader 0
strict 0
- HTML-Tagset-3.20
- pathname: P/PE/PETDANCE/HTML-Tagset-3.20.tar.gz
+ HTML-Tagset-3.24
+ pathname: P/PE/PETDANCE/HTML-Tagset-3.24.tar.gz
provides:
- HTML::Tagset 3.20
+ HTML::Tagset 3.24
requirements:
- ExtUtils::MakeMaker 0
- HTTP-CookieJar-0.014
- pathname: D/DA/DAGOLDEN/HTTP-CookieJar-0.014.tar.gz
- provides:
- HTTP::CookieJar 0.014
- HTTP::CookieJar::LWP 0.014
- requirements:
- Carp 0
- ExtUtils::MakeMaker 6.17
- HTTP::Date 0
- Time::Local 1.1901
- parent 0
- perl 5.008001
- strict 0
- warnings 0
- HTTP-Cookies-6.10
- pathname: O/OA/OALDERS/HTTP-Cookies-6.10.tar.gz
+ ExtUtils::MakeMaker 6.46
+ perl 5.010001
+ HTTP-Cookies-6.11
+ pathname: O/OA/OALDERS/HTTP-Cookies-6.11.tar.gz
provides:
- HTTP::Cookies 6.10
- HTTP::Cookies::Microsoft 6.10
- HTTP::Cookies::Netscape 6.10
+ HTTP::Cookies 6.11
+ HTTP::Cookies::Microsoft 6.11
+ HTTP::Cookies::Netscape 6.11
requirements:
Carp 0
ExtUtils::MakeMaker 0
@@ -1063,19 +1093,19 @@ DISTRIBUTIONS
Time::Zone 0
perl 5.006002
strict 0
- HTTP-Message-6.44
- pathname: O/OA/OALDERS/HTTP-Message-6.44.tar.gz
- provides:
- HTTP::Config 6.44
- HTTP::Headers 6.44
- HTTP::Headers::Auth 6.44
- HTTP::Headers::ETag 6.44
- HTTP::Headers::Util 6.44
- HTTP::Message 6.44
- HTTP::Request 6.44
- HTTP::Request::Common 6.44
- HTTP::Response 6.44
- HTTP::Status 6.44
+ HTTP-Message-7.00
+ pathname: O/OA/OALDERS/HTTP-Message-7.00.tar.gz
+ provides:
+ HTTP::Config 7.00
+ HTTP::Headers 7.00
+ HTTP::Headers::Auth 7.00
+ HTTP::Headers::ETag 7.00
+ HTTP::Headers::Util 7.00
+ HTTP::Message 7.00
+ HTTP::Request 7.00
+ HTTP::Request::Common 7.00
+ HTTP::Response 7.00
+ HTTP::Status 7.00
requirements:
Carp 0
Clone 0.46
@@ -1136,24 +1166,37 @@ DISTRIBUTIONS
Exporter 5.57
ExtUtils::MakeMaker 0
perl 5.008
- IO-Socket-SSL-2.083
- pathname: S/SU/SULLR/IO-Socket-SSL-2.083.tar.gz
+ IO-Socket-SSL-2.095
+ pathname: S/SU/SULLR/IO-Socket-SSL-2.095.tar.gz
provides:
- IO::Socket::SSL 2.083
+ IO::Socket::SSL 2.095
IO::Socket::SSL::Intercept 2.056
- IO::Socket::SSL::OCSP_Cache 2.083
- IO::Socket::SSL::OCSP_Resolver 2.083
+ IO::Socket::SSL::OCSP_Cache 2.095
+ IO::Socket::SSL::OCSP_Resolver 2.095
IO::Socket::SSL::PublicSuffix undef
- IO::Socket::SSL::SSL_Context 2.083
- IO::Socket::SSL::SSL_HANDLE 2.083
- IO::Socket::SSL::Session_Cache 2.083
- IO::Socket::SSL::Trace 2.083
+ IO::Socket::SSL::SSL_Context 2.095
+ IO::Socket::SSL::SSL_HANDLE 2.095
+ IO::Socket::SSL::Session_Cache 2.095
+ IO::Socket::SSL::Trace 2.095
IO::Socket::SSL::Utils 2.015
requirements:
ExtUtils::MakeMaker 0
- Mozilla::CA 0
Net::SSLeay 1.46
Scalar::Util 0
+ IO-Socket-Socks-0.74
+ pathname: O/OL/OLEG/IO-Socket-Socks-0.74.tar.gz
+ provides:
+ IO::Socket::Socks 0.74
+ IO::Socket::Socks::Debug 0.74
+ IO::Socket::Socks::Error 0.74
+ IO::Socket::Socks::ReadOnlyVar 0.74
+ IO::Socket::Socks::SocketClassVar 0.74
+ requirements:
+ ExtUtils::MakeMaker 6.52
+ IO::Select 0
+ Socket 1.94
+ Test::More 0.88
+ constant 1.03
IO-String-1.08
pathname: G/GA/GAAS/IO-String-1.08.tar.gz
provides:
@@ -1189,11 +1232,11 @@ DISTRIBUTIONS
Scalar::Util 0
perl 5.006002
strict 0
- LWP-Protocol-https-6.11
- pathname: O/OA/OALDERS/LWP-Protocol-https-6.11.tar.gz
+ LWP-Protocol-https-6.14
+ pathname: O/OA/OALDERS/LWP-Protocol-https-6.14.tar.gz
provides:
- LWP::Protocol::https 6.11
- LWP::Protocol::https::Socket 6.11
+ LWP::Protocol::https 6.14
+ LWP::Protocol::https::Socket 6.14
requirements:
ExtUtils::MakeMaker 0
IO::Socket::SSL 1.970
@@ -1245,6 +1288,15 @@ DISTRIBUTIONS
requirements:
Exporter 5.57
Module::Build 0.4004
+ MIME-Base32-1.303
+ pathname: R/RE/REHSACK/MIME-Base32-1.303.tar.gz
+ provides:
+ MIME::Base32 1.303
+ requirements:
+ Exporter 0
+ ExtUtils::MakeMaker 0
+ perl 5.008001
+ utf8 0
MRO-Compat-0.15
pathname: H/HA/HAARG/MRO-Compat-0.15.tar.gz
provides:
@@ -1297,10 +1349,10 @@ DISTRIBUTIONS
Text::ParseWords 0
perl 5.006001
version 0.87
- Module-Build-Tiny-0.046
- pathname: L/LE/LEONT/Module-Build-Tiny-0.046.tar.gz
+ Module-Build-Tiny-0.052
+ pathname: L/LE/LEONT/Module-Build-Tiny-0.052.tar.gz
provides:
- Module::Build::Tiny 0.046
+ Module::Build::Tiny 0.052
requirements:
CPAN::Meta 0
DynaLoader 0
@@ -1333,24 +1385,22 @@ DISTRIBUTIONS
Try::Tiny 0
strict 0
warnings 0
- Module-Runtime-0.016
- pathname: Z/ZE/ZEFRAM/Module-Runtime-0.016.tar.gz
+ Module-Runtime-0.018
+ pathname: H/HA/HAARG/Module-Runtime-0.018.tar.gz
provides:
- Module::Runtime 0.016
+ Module::Runtime 0.018
requirements:
- Module::Build 0
- Test::More 0.41
- perl 5.006
- strict 0
- warnings 0
- Mojolicious-9.33
- pathname: S/SR/SRI/Mojolicious-9.33.tar.gz
+ ExtUtils::MakeMaker 0
+ perl 5.006000
+ Mojolicious-9.41
+ pathname: S/SR/SRI/Mojolicious-9.41.tar.gz
provides:
Mojo undef
Mojo::Asset undef
Mojo::Asset::File undef
Mojo::Asset::Memory undef
Mojo::Base undef
+ Mojo::BaseUtil undef
Mojo::ByteStream undef
Mojo::Cache undef
Mojo::Collection undef
@@ -1390,6 +1440,7 @@ DISTRIBUTIONS
Mojo::Reactor undef
Mojo::Reactor::EV undef
Mojo::Reactor::Poll undef
+ Mojo::SSE undef
Mojo::Server undef
Mojo::Server::CGI undef
Mojo::Server::Daemon undef
@@ -1412,7 +1463,7 @@ DISTRIBUTIONS
Mojo::UserAgent::Transactor undef
Mojo::Util undef
Mojo::WebSocket undef
- Mojolicious 9.33
+ Mojolicious 9.41
Mojolicious::Command undef
Mojolicious::Command::Author::cpanify undef
Mojolicious::Command::Author::generate undef
@@ -1461,12 +1512,6 @@ DISTRIBUTIONS
IO::Socket::IP 0.37
Sub::Util 1.41
perl 5.016
- Mozilla-CA-20230821
- pathname: L/LW/LWP/Mozilla-CA-20230821.tar.gz
- provides:
- Mozilla::CA 20230821
- requirements:
- ExtUtils::MakeMaker 0
Net-HTTP-6.23
pathname: O/OA/OALDERS/Net-HTTP-6.23.tar.gz
provides:
@@ -1485,11 +1530,11 @@ DISTRIBUTIONS
perl 5.006002
strict 0
warnings 0
- Net-SSLeay-1.92
- pathname: C/CH/CHRISN/Net-SSLeay-1.92.tar.gz
+ Net-SSLeay-1.94
+ pathname: C/CH/CHRISN/Net-SSLeay-1.94.tar.gz
provides:
- Net::SSLeay 1.92
- Net::SSLeay::Handle 1.92
+ Net::SSLeay 1.94
+ Net::SSLeay::Handle 1.94
requirements:
English 0
ExtUtils::MakeMaker 0
@@ -1544,6 +1589,25 @@ DISTRIBUTIONS
Scalar::Util 1.18
XSLoader 0.22
parent 0
+ Params-Validate-1.31
+ pathname: D/DR/DROLSKY/Params-Validate-1.31.tar.gz
+ provides:
+ Params::Validate 1.31
+ Params::Validate::Constants 1.31
+ Params::Validate::PP 1.31
+ Params::Validate::XS 1.31
+ requirements:
+ Carp 0
+ Exporter 0
+ ExtUtils::CBuilder 0
+ Module::Build 0.4227
+ Module::Implementation 0
+ Scalar::Util 1.10
+ XSLoader 0
+ perl 5.008001
+ strict 0
+ vars 0
+ warnings 0
Params-ValidationCompiler-0.31
pathname: D/DR/DROLSKY/Params-ValidationCompiler-0.31.tar.gz
provides:
@@ -1562,11 +1626,11 @@ DISTRIBUTIONS
overload 0
strict 0
warnings 0
- Path-Tiny-0.144
- pathname: D/DA/DAGOLDEN/Path-Tiny-0.144.tar.gz
+ Path-Tiny-0.150
+ pathname: D/DA/DAGOLDEN/Path-Tiny-0.150.tar.gz
provides:
- Path::Tiny 0.144
- Path::Tiny::Error 0.144
+ Path::Tiny 0.150
+ Path::Tiny::Error 0.150
requirements:
Carp 0
Cwd 0
@@ -1589,6 +1653,14 @@ DISTRIBUTIONS
strict 0
warnings 0
warnings::register 0
+ PkgConfig-0.26026
+ pathname: P/PL/PLICEASE/PkgConfig-0.26026.tar.gz
+ provides:
+ PkgConfig 0.26026
+ requirements:
+ ExtUtils::MakeMaker 6.56
+ Test::More 0.94
+ perl 5.006000
Role-Tiny-2.002004
pathname: H/HA/HAARG/Role-Tiny-2.002004.tar.gz
provides:
@@ -1597,52 +1669,56 @@ DISTRIBUTIONS
requirements:
Exporter 5.57
perl 5.006
- Specio-0.48
- pathname: D/DR/DROLSKY/Specio-0.48.tar.gz
- provides:
- Specio 0.48
- Specio::Coercion 0.48
- Specio::Constraint::AnyCan 0.48
- Specio::Constraint::AnyDoes 0.48
- Specio::Constraint::AnyIsa 0.48
- Specio::Constraint::Enum 0.48
- Specio::Constraint::Intersection 0.48
- Specio::Constraint::ObjectCan 0.48
- Specio::Constraint::ObjectDoes 0.48
- Specio::Constraint::ObjectIsa 0.48
- Specio::Constraint::Parameterizable 0.48
- Specio::Constraint::Parameterized 0.48
- Specio::Constraint::Role::CanType 0.48
- Specio::Constraint::Role::DoesType 0.48
- Specio::Constraint::Role::Interface 0.48
- Specio::Constraint::Role::IsaType 0.48
- Specio::Constraint::Simple 0.48
- Specio::Constraint::Structurable 0.48
- Specio::Constraint::Structured 0.48
- Specio::Constraint::Union 0.48
- Specio::Declare 0.48
- Specio::DeclaredAt 0.48
- Specio::Exception 0.48
- Specio::Exporter 0.48
- Specio::Helpers 0.48
- Specio::Library::Builtins 0.48
- Specio::Library::Numeric 0.48
- Specio::Library::Perl 0.48
- Specio::Library::String 0.48
- Specio::Library::Structured 0.48
- Specio::Library::Structured::Dict 0.48
- Specio::Library::Structured::Map 0.48
- Specio::Library::Structured::Tuple 0.48
- Specio::OO 0.48
- Specio::PartialDump 0.48
- Specio::Registry 0.48
- Specio::Role::Inlinable 0.48
- Specio::Subs 0.48
- Specio::TypeChecks 0.48
- Test::Specio 0.48
+ Specio-0.52
+ pathname: D/DR/DROLSKY/Specio-0.52.tar.gz
+ provides:
+ Specio 0.52
+ Specio::Coercion 0.52
+ Specio::Constraint::AnyCan 0.52
+ Specio::Constraint::AnyDoes 0.52
+ Specio::Constraint::AnyIsa 0.52
+ Specio::Constraint::Enum 0.52
+ Specio::Constraint::Intersection 0.52
+ Specio::Constraint::ObjectCan 0.52
+ Specio::Constraint::ObjectDoes 0.52
+ Specio::Constraint::ObjectIsa 0.52
+ Specio::Constraint::Parameterizable 0.52
+ Specio::Constraint::Parameterized 0.52
+ Specio::Constraint::Role::CanType 0.52
+ Specio::Constraint::Role::DoesType 0.52
+ Specio::Constraint::Role::Interface 0.52
+ Specio::Constraint::Role::IsaType 0.52
+ Specio::Constraint::Simple 0.52
+ Specio::Constraint::Structurable 0.52
+ Specio::Constraint::Structured 0.52
+ Specio::Constraint::Union 0.52
+ Specio::Declare 0.52
+ Specio::DeclaredAt 0.52
+ Specio::Exception 0.52
+ Specio::Exporter 0.52
+ Specio::Helpers 0.52
+ Specio::Library::Builtins 0.52
+ Specio::Library::Numeric 0.52
+ Specio::Library::Perl 0.52
+ Specio::Library::String 0.52
+ Specio::Library::Structured 0.52
+ Specio::Library::Structured::Dict 0.52
+ Specio::Library::Structured::Map 0.52
+ Specio::Library::Structured::Tuple 0.52
+ Specio::OO 0.52
+ Specio::PP 0.52
+ Specio::PartialDump 0.52
+ Specio::Registry 0.52
+ Specio::Role::Inlinable 0.52
+ Specio::Subs 0.52
+ Specio::TypeChecks 0.52
+ Specio::XS 0.52
+ Test::Specio 0.52
requirements:
B 0
Carp 0
+ Clone 0
+ Clone::PP 0
Devel::StackTrace 0
Eval::Closure 0
Exporter 0
@@ -1650,11 +1726,11 @@ DISTRIBUTIONS
IO::File 0
List::Util 1.33
MRO::Compat 0
+ Module::Implementation 0
Module::Runtime 0
Role::Tiny 1.003003
Role::Tiny::With 0
Scalar::Util 0
- Storable 0
Sub::Quote 0
Test::Fatal 0
Test::More 0.96
@@ -1667,18 +1743,18 @@ DISTRIBUTIONS
strict 0
version 0.83
warnings 0
- Sub-Exporter-0.990
- pathname: R/RJ/RJBS/Sub-Exporter-0.990.tar.gz
+ Sub-Exporter-0.991
+ pathname: R/RJ/RJBS/Sub-Exporter-0.991.tar.gz
provides:
- Sub::Exporter 0.990
- Sub::Exporter::Util 0.990
+ Sub::Exporter 0.991
+ Sub::Exporter::Util 0.991
requirements:
Carp 0
Data::OptList 0.100
ExtUtils::MakeMaker 6.78
Params::Util 0.14
Sub::Install 0.92
- perl 5.008000
+ perl 5.012
strict 0
warnings 0
Sub-Exporter-Progressive-0.001013
@@ -1687,13 +1763,6 @@ DISTRIBUTIONS
Sub::Exporter::Progressive 0.001013
requirements:
ExtUtils::MakeMaker 0
- Sub-Identify-0.14
- pathname: R/RG/RGARCIA/Sub-Identify-0.14.tar.gz
- provides:
- Sub::Identify 0.14
- requirements:
- ExtUtils::MakeMaker 0
- Test::More 0
Sub-Install-0.929
pathname: R/RJ/RJBS/Sub-Install-0.929.tar.gz
provides:
@@ -1706,26 +1775,25 @@ DISTRIBUTIONS
perl 5.008000
strict 0
warnings 0
- Sub-Quote-2.006008
- pathname: H/HA/HAARG/Sub-Quote-2.006008.tar.gz
+ Sub-Quote-2.006009
+ pathname: H/HA/HAARG/Sub-Quote-2.006009.tar.gz
provides:
- Sub::Defer 2.006008
- Sub::Quote 2.006008
+ Sub::Defer 2.006009
+ Sub::Quote 2.006009
requirements:
ExtUtils::MakeMaker 0
Scalar::Util 0
perl 5.006
- Test-Compile-v3.3.1
- pathname: E/EG/EGILES/Test-Compile-v3.3.1.tar.gz
+ Test-Compile-v3.3.3
+ pathname: E/EG/EGILES/Test-Compile-v3.3.3.tar.gz
provides:
- Test::Compile v3.3.1
- Test::Compile::Internal v3.3.1
+ Test::Compile v3.3.3
+ Test::Compile::Internal v3.3.3
requirements:
Exporter 5.68
Module::Build 0.38
parent 0.225
perl v5.10.0
- version 0.77
Test-Fatal-0.017
pathname: R/RJ/RJBS/Test-Fatal-0.017.tar.gz
provides:
@@ -1762,17 +1830,17 @@ DISTRIBUTIONS
Test::Builder::Tester 1.02
Test::More 0.62
perl 5.008
- Text-CSV-2.03
- pathname: I/IS/ISHIGAKI/Text-CSV-2.03.tar.gz
+ Text-CSV-2.06
+ pathname: I/IS/ISHIGAKI/Text-CSV-2.06.tar.gz
provides:
- Text::CSV 2.03
- Text::CSV::ErrorDiag 2.03
- Text::CSV_PP 2.03
+ Text::CSV 2.06
+ Text::CSV::ErrorDiag 2.06
+ Text::CSV_PP 2.06
requirements:
ExtUtils::MakeMaker 0
IO::Handle 0
Test::Harness 0
- Test::More 0.71
+ Test::More 0.92
perl 5.006001
Text-LevenshteinXS-0.03
pathname: J/JG/JGOLDBERG/Text-LevenshteinXS-0.03.tar.gz
@@ -1826,35 +1894,44 @@ DISTRIBUTIONS
TimeDate 1.21
requirements:
ExtUtils::MakeMaker 0
- Travel-Status-DE-DBWagenreihung-0.08
- pathname: D/DE/DERF/Travel-Status-DE-DBWagenreihung-0.08.tar.gz
+ Travel-Status-DE-DBRIS-0.13
+ pathname: D/DE/DERF/Travel-Status-DE-DBRIS-0.13.tar.gz
provides:
- Travel::Status::DE::DBWagenreihung 0.08
- Travel::Status::DE::DBWagenreihung::Section 0.08
- Travel::Status::DE::DBWagenreihung::Wagon 0.08
+ Travel::Status::DE::DBRIS 0.13
+ Travel::Status::DE::DBRIS::Formation 0.13
+ Travel::Status::DE::DBRIS::Formation::Carriage 0.13
+ Travel::Status::DE::DBRIS::Formation::Group 0.13
+ Travel::Status::DE::DBRIS::Formation::Sector 0.13
+ Travel::Status::DE::DBRIS::Journey 0.13
+ Travel::Status::DE::DBRIS::JourneyAtStop 0.13
+ Travel::Status::DE::DBRIS::Location 0.13
requirements:
Carp 0
- Class::Accessor 0
+ Class::Accessor 0.16
+ DateTime 0
+ DateTime::Format::Strptime 0
Getopt::Long 0
JSON 0
+ LWP::Protocol::https 0
LWP::UserAgent 0
List::Util 0
Module::Build 0.4
Test::Compile 0
Test::More 0
Test::Pod 0
- Travel::Status::DE::IRIS 1.2
perl v5.20.0
- Travel-Status-DE-DeutscheBahn-4.16
- pathname: D/DE/DERF/Travel-Status-DE-DeutscheBahn-4.16.tar.gz
- provides:
- Travel::Status::DE::DeutscheBahn 4.16
- Travel::Status::DE::HAFAS 4.16
- Travel::Status::DE::HAFAS::Journey 4.16
- Travel::Status::DE::HAFAS::Message 4.16
- Travel::Status::DE::HAFAS::Polyline 4.16
- Travel::Status::DE::HAFAS::Stop 4.16
- Travel::Status::DE::HAFAS::StopFinder 4.16
+ Travel-Status-DE-HAFAS-6.22
+ pathname: D/DE/DERF/Travel-Status-DE-HAFAS-6.22.tar.gz
+ provides:
+ Travel::Status::DE::HAFAS 6.22
+ Travel::Status::DE::HAFAS::Journey 6.22
+ Travel::Status::DE::HAFAS::Location 6.22
+ Travel::Status::DE::HAFAS::Message 6.22
+ Travel::Status::DE::HAFAS::Polyline 6.22
+ Travel::Status::DE::HAFAS::Product 6.22
+ Travel::Status::DE::HAFAS::Services 6.22
+ Travel::Status::DE::HAFAS::Stop 6.22
+ Travel::Status::DE::HAFAS::StopFinder 6.22
requirements:
Carp 0
Class::Accessor 0.16
@@ -1872,12 +1949,12 @@ DISTRIBUTIONS
Test::More 0
Test::Pod 0
perl v5.14.0
- Travel-Status-DE-IRIS-1.89
- pathname: D/DE/DERF/Travel-Status-DE-IRIS-1.89.tar.gz
+ Travel-Status-DE-IRIS-1.98
+ pathname: D/DE/DERF/Travel-Status-DE-IRIS-1.98.tar.gz
provides:
- Travel::Status::DE::IRIS 1.89
- Travel::Status::DE::IRIS::Result 1.89
- Travel::Status::DE::IRIS::Stations 1.89
+ Travel::Status::DE::IRIS 1.98
+ Travel::Status::DE::IRIS::Result 1.98
+ Travel::Status::DE::IRIS::Stations 1.98
requirements:
Carp 0
Class::Accessor 0
@@ -1904,10 +1981,61 @@ DISTRIBUTIONS
Text::LevenshteinXS 0
XML::LibXML 0
perl v5.14.2
- Try-Tiny-0.31
- pathname: E/ET/ETHER/Try-Tiny-0.31.tar.gz
+ Travel-Status-DE-VRR-3.14
+ pathname: D/DE/DERF/Travel-Status-DE-VRR-3.14.tar.gz
+ provides:
+ Travel::Status::DE::EFA 3.14
+ Travel::Status::DE::EFA::Departure 3.14
+ Travel::Status::DE::EFA::Info 3.14
+ Travel::Status::DE::EFA::Line 3.14
+ Travel::Status::DE::EFA::Services 3.14
+ Travel::Status::DE::EFA::Stop 3.14
+ Travel::Status::DE::EFA::Trip 3.14
+ Travel::Status::DE::VRR 3.14
+ requirements:
+ Carp 0
+ Class::Accessor 0
+ DateTime 0
+ DateTime::Format::Strptime 0
+ File::Slurp 0
+ Getopt::Long 0
+ JSON 0
+ LWP::Protocol::https 0
+ LWP::UserAgent 0
+ List::Util 0
+ Module::Build 0.4
+ Test::More 0
+ perl v5.10.1
+ Travel-Status-MOTIS-0.03
+ pathname: D/DE/DERF/Travel-Status-MOTIS-0.03.tar.gz
+ provides:
+ Travel::Status::MOTIS 0.03
+ Travel::Status::MOTIS::Polyline 0.03
+ Travel::Status::MOTIS::Services 0.03
+ Travel::Status::MOTIS::Stop 0.03
+ Travel::Status::MOTIS::Stopover 0.03
+ Travel::Status::MOTIS::Trip 0.03
+ Travel::Status::MOTIS::TripAtStopover 0.03
+ requirements:
+ Carp 0
+ Class::Accessor 0.16
+ DateTime 0
+ DateTime::Format::ISO8601 0
+ Getopt::Long 0
+ JSON 0
+ LWP::Protocol::https 0
+ LWP::UserAgent 0
+ List::Util 0
+ Module::Build 0.4
+ Test::Compile 0
+ Test::More 0
+ Test::Pod 0
+ URI 0
+ perl v5.20.0
+ Try-Tiny-0.32
+ pathname: E/ET/ETHER/Try-Tiny-0.32.tar.gz
provides:
- Try::Tiny 0.31
+ Try::Tiny 0.32
requirements:
Carp 0
Exporter 5.57
@@ -1926,55 +2054,63 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
common::sense 0
- URI-5.21
- pathname: O/OA/OALDERS/URI-5.21.tar.gz
- provides:
- URI 5.21
- URI::Escape 5.21
- URI::Heuristic 5.21
- URI::IRI 5.21
- URI::QueryParam 5.21
- URI::Split 5.21
- URI::URL 5.21
- URI::WithBase 5.21
- URI::data 5.21
- URI::file 5.21
- URI::file::Base 5.21
- URI::file::FAT 5.21
- URI::file::Mac 5.21
- URI::file::OS2 5.21
- URI::file::QNX 5.21
- URI::file::Unix 5.21
- URI::file::Win32 5.21
- URI::ftp 5.21
- URI::gopher 5.21
- URI::http 5.21
- URI::https 5.21
- URI::icap 5.21
- URI::icaps 5.21
- URI::ldap 5.21
- URI::ldapi 5.21
- URI::ldaps 5.21
- URI::mailto 5.21
- URI::mms 5.21
- URI::news 5.21
- URI::nntp 5.21
- URI::nntps 5.21
- URI::pop 5.21
- URI::rlogin 5.21
- URI::rsync 5.21
- URI::rtsp 5.21
- URI::rtspu 5.21
- URI::sftp 5.21
- URI::sip 5.21
- URI::sips 5.21
- URI::snews 5.21
- URI::ssh 5.21
- URI::telnet 5.21
- URI::tn3270 5.21
- URI::urn 5.21
- URI::urn::isbn 5.21
- URI::urn::oid 5.21
+ URI-5.32
+ pathname: O/OA/OALDERS/URI-5.32.tar.gz
+ provides:
+ URI 5.32
+ URI::Escape 5.32
+ URI::Heuristic 5.32
+ URI::IRI 5.32
+ URI::QueryParam 5.32
+ URI::Split 5.32
+ URI::URL 5.32
+ URI::WithBase 5.32
+ URI::data 5.32
+ URI::file 5.32
+ URI::file::Base 5.32
+ URI::file::FAT 5.32
+ URI::file::Mac 5.32
+ URI::file::OS2 5.32
+ URI::file::QNX 5.32
+ URI::file::Unix 5.32
+ URI::file::Win32 5.32
+ URI::ftp 5.32
+ URI::ftpes 5.32
+ URI::ftps 5.32
+ URI::geo 5.32
+ URI::gopher 5.32
+ URI::http 5.32
+ URI::https 5.32
+ URI::icap 5.32
+ URI::icaps 5.32
+ URI::irc 5.32
+ URI::ircs 5.32
+ URI::ldap 5.32
+ URI::ldapi 5.32
+ URI::ldaps 5.32
+ URI::mailto 5.32
+ URI::mms 5.32
+ URI::news 5.32
+ URI::nntp 5.32
+ URI::nntps 5.32
+ URI::otpauth 5.32
+ URI::pop 5.32
+ URI::rlogin 5.32
+ URI::rsync 5.32
+ URI::rtsp 5.32
+ URI::rtspu 5.32
+ URI::scp 5.32
+ URI::sftp 5.32
+ URI::sip 5.32
+ URI::sips 5.32
+ URI::smb 5.32
+ URI::snews 5.32
+ URI::ssh 5.32
+ URI::telnet 5.32
+ URI::tn3270 5.32
+ URI::urn 5.32
+ URI::urn::isbn 5.32
+ URI::urn::oid 5.32
requirements:
Carp 0
Cwd 0
@@ -1982,6 +2118,7 @@ DISTRIBUTIONS
Encode 0
Exporter 5.57
ExtUtils::MakeMaker 0
+ MIME::Base32 0
MIME::Base64 2
Net::Domain 0
Scalar::Util 0
@@ -1993,10 +2130,10 @@ DISTRIBUTIONS
strict 0
utf8 0
warnings 0
- Variable-Magic-0.63
- pathname: V/VP/VPIT/Variable-Magic-0.63.tar.gz
+ Variable-Magic-0.64
+ pathname: V/VP/VPIT/Variable-Magic-0.64.tar.gz
provides:
- Variable::Magic 0.63
+ Variable::Magic 0.64
requirements:
Carp 0
Config 0
@@ -2024,45 +2161,45 @@ DISTRIBUTIONS
Fcntl 0
URI 1.10
perl 5.008001
- XML-LibXML-2.0209
- pathname: S/SH/SHLOMIF/XML-LibXML-2.0209.tar.gz
- provides:
- XML::LibXML 2.0209
- XML::LibXML::Attr 2.0209
- XML::LibXML::AttributeHash 2.0209
- XML::LibXML::Boolean 2.0209
- XML::LibXML::CDATASection 2.0209
- XML::LibXML::Comment 2.0209
- XML::LibXML::Common 2.0209
- XML::LibXML::Devel 2.0209
- XML::LibXML::Document 2.0209
- XML::LibXML::DocumentFragment 2.0209
- XML::LibXML::Dtd 2.0209
- XML::LibXML::Element 2.0209
- XML::LibXML::ErrNo 2.0209
- XML::LibXML::Error 2.0209
- XML::LibXML::InputCallback 2.0209
- XML::LibXML::Literal 2.0209
- XML::LibXML::NamedNodeMap 2.0209
- XML::LibXML::Namespace 2.0209
- XML::LibXML::Node 2.0209
- XML::LibXML::NodeList 2.0209
- XML::LibXML::Number 2.0209
- XML::LibXML::PI 2.0209
- XML::LibXML::Pattern 2.0209
- XML::LibXML::Reader 2.0209
- XML::LibXML::RegExp 2.0209
- XML::LibXML::RelaxNG 2.0209
- XML::LibXML::SAX 2.0209
- XML::LibXML::SAX::AttributeNode 2.0209
- XML::LibXML::SAX::Builder 2.0209
- XML::LibXML::SAX::Generator 2.0209
- XML::LibXML::SAX::Parser 2.0209
- XML::LibXML::Schema 2.0209
- XML::LibXML::Text 2.0209
- XML::LibXML::XPathContext 2.0209
- XML::LibXML::XPathExpression 2.0209
- XML::LibXML::_SAXParser 2.0209
+ XML-LibXML-2.0210
+ pathname: S/SH/SHLOMIF/XML-LibXML-2.0210.tar.gz
+ provides:
+ XML::LibXML 2.0210
+ XML::LibXML::Attr 2.0210
+ XML::LibXML::AttributeHash 2.0210
+ XML::LibXML::Boolean 2.0210
+ XML::LibXML::CDATASection 2.0210
+ XML::LibXML::Comment 2.0210
+ XML::LibXML::Common 2.0210
+ XML::LibXML::Devel 2.0210
+ XML::LibXML::Document 2.0210
+ XML::LibXML::DocumentFragment 2.0210
+ XML::LibXML::Dtd 2.0210
+ XML::LibXML::Element 2.0210
+ XML::LibXML::ErrNo 2.0210
+ XML::LibXML::Error 2.0210
+ XML::LibXML::InputCallback 2.0210
+ XML::LibXML::Literal 2.0210
+ XML::LibXML::NamedNodeMap 2.0210
+ XML::LibXML::Namespace 2.0210
+ XML::LibXML::Node 2.0210
+ XML::LibXML::NodeList 2.0210
+ XML::LibXML::Number 2.0210
+ XML::LibXML::PI 2.0210
+ XML::LibXML::Pattern 2.0210
+ XML::LibXML::Reader 2.0210
+ XML::LibXML::RegExp 2.0210
+ XML::LibXML::RelaxNG 2.0210
+ XML::LibXML::SAX 2.0210
+ XML::LibXML::SAX::AttributeNode 2.0210
+ XML::LibXML::SAX::Builder 2.0210
+ XML::LibXML::SAX::Generator 2.0210
+ XML::LibXML::SAX::Parser 2.0210
+ XML::LibXML::Schema 2.0210
+ XML::LibXML::Text 2.0210
+ XML::LibXML::XPathContext 2.0210
+ XML::LibXML::XPathExpression 2.0210
+ XML::LibXML::_SAXParser 2.0210
requirements:
Alien::Base::Wrapper 0
Alien::Libxml2 0.14
@@ -2172,32 +2309,32 @@ DISTRIBUTIONS
XSLoader 0
lib 0
perl 5.008001
- libwww-perl-6.72
- pathname: O/OA/OALDERS/libwww-perl-6.72.tar.gz
- provides:
- LWP 6.72
- LWP::Authen::Basic 6.72
- LWP::Authen::Digest 6.72
- LWP::Authen::Ntlm 6.72
- LWP::ConnCache 6.72
- LWP::Debug 6.72
- LWP::Debug::TraceHTTP 6.72
- LWP::DebugFile 6.72
- LWP::MemberMixin 6.72
- LWP::Protocol 6.72
- LWP::Protocol::cpan 6.72
- LWP::Protocol::data 6.72
- LWP::Protocol::file 6.72
- LWP::Protocol::ftp 6.72
- LWP::Protocol::gopher 6.72
- LWP::Protocol::http 6.72
- LWP::Protocol::loopback 6.72
- LWP::Protocol::mailto 6.72
- LWP::Protocol::nntp 6.72
- LWP::Protocol::nogo 6.72
- LWP::RobotUA 6.72
- LWP::Simple 6.72
- LWP::UserAgent 6.72
+ libwww-perl-6.79
+ pathname: O/OA/OALDERS/libwww-perl-6.79.tar.gz
+ provides:
+ LWP 6.79
+ LWP::Authen::Basic 6.79
+ LWP::Authen::Digest 6.79
+ LWP::Authen::Ntlm 6.79
+ LWP::ConnCache 6.79
+ LWP::Debug 6.79
+ LWP::Debug::TraceHTTP 6.79
+ LWP::DebugFile 6.79
+ LWP::MemberMixin 6.79
+ LWP::Protocol 6.79
+ LWP::Protocol::cpan 6.79
+ LWP::Protocol::data 6.79
+ LWP::Protocol::file 6.79
+ LWP::Protocol::ftp 6.79
+ LWP::Protocol::gopher 6.79
+ LWP::Protocol::http 6.79
+ LWP::Protocol::loopback 6.79
+ LWP::Protocol::mailto 6.79
+ LWP::Protocol::nntp 6.79
+ LWP::Protocol::nogo 6.79
+ LWP::RobotUA 6.79
+ LWP::Simple 6.79
+ LWP::UserAgent 6.79
requirements:
Digest::MD5 0
Encode 2.12
@@ -2209,7 +2346,6 @@ DISTRIBUTIONS
Getopt::Long 0
HTML::Entities 0
HTML::HeadParser 3.71
- HTTP::CookieJar::LWP 0
HTTP::Cookies 6
HTTP::Date 6
HTTP::Negotiate 6
@@ -2248,15 +2384,15 @@ DISTRIBUTIONS
perl 5.008001
strict 0
warnings 0
- namespace-autoclean-0.29
- pathname: E/ET/ETHER/namespace-autoclean-0.29.tar.gz
+ namespace-autoclean-0.31
+ pathname: E/ET/ETHER/namespace-autoclean-0.31.tar.gz
provides:
- namespace::autoclean 0.29
+ namespace::autoclean 0.31
requirements:
+ B 0
B::Hooks::EndOfScope 0.12
ExtUtils::MakeMaker 0
List::Util 0
- Sub::Identify 0
namespace::clean 0.20
perl 5.006
strict 0
diff --git a/lib/DBInfoscreen.pm b/lib/DBInfoscreen.pm
index 687d583..18a2c87 100644
--- a/lib/DBInfoscreen.pm
+++ b/lib/DBInfoscreen.pm
@@ -7,8 +7,10 @@ package DBInfoscreen;
use Mojo::Base 'Mojolicious';
use Cache::File;
+use DBInfoscreen::Helper::DBRIS;
use DBInfoscreen::Helper::EFA;
use DBInfoscreen::Helper::HAFAS;
+use DBInfoscreen::Helper::MOTIS;
use DBInfoscreen::Helper::Wagonorder;
use File::Slurp qw(read_file);
use JSON;
@@ -47,15 +49,14 @@ sub startup {
before_dispatch => sub {
my ($self) = @_;
- # The "theme" cookie is set client-side if the theme we delivered was
- # changed by dark mode detection or by using the theme switcher. It's
- # not part of Mojolicious' session data (and can't be, due to
- # signing and HTTPOnly), so we need to add it here.
+ # The "theme" cookie is set client-side if the theme we delivered was
+ # changed by dark mode detection or by using the theme switcher. It's
+ # not part of Mojolicious' session data (and can't be, due to
+ # signing and HTTPOnly), so we need to add it here.
for my $cookie ( @{ $self->req->cookies } ) {
if ( $cookie->name eq 'theme' ) {
$self->session( theme => $cookie->value );
- return;
}
}
}
@@ -86,39 +87,37 @@ sub startup {
);
$self->attr(
- ice_type_map => sub {
- if ( -r 'share/zugbildungsplan.json' ) {
- my $ice_type_map = JSON->new->utf8->decode(
- scalar read_file('share/zugbildungsplan.json') );
- my $ret = {};
- while ( my ( $k, $v ) = each %{ $ice_type_map->{train} } ) {
- if ( $v->{type} ) {
- $ret->{$k} = [
- $v->{type}, $v->{shortType},
- exists $v->{wagons} ? 1 : 0
- ];
- }
- }
- return $ret;
- }
- return {};
+ dbdb_wagon => sub {
+ return JSON->new->utf8->decode(
+ scalar read_file('share/dbdb_wagen.json') );
}
);
- $self->attr(
- train_details_db => sub {
- if ( -r 'share/zugbildungsplan.json' ) {
- return JSON->new->utf8->decode(
- scalar read_file('share/zugbildungsplan.json') )->{train};
- }
- return {};
+ $self->helper(
+ dbris => sub {
+ my ($self) = @_;
+ state $efa = DBInfoscreen::Helper::DBRIS->new(
+ log => $self->app->log,
+ main_cache => $self->app->cache_iris_main,
+ realtime_cache => $self->app->cache_iris_rt,
+ root_url => $self->url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->config->{version},
+ );
}
);
- $self->attr(
- dbdb_wagon => sub {
- return JSON->new->utf8->decode(
- scalar read_file('share/dbdb_wagen.json') );
+ $self->helper(
+ motis => sub {
+ my ($self) = @_;
+ state $motis = DBInfoscreen::Helper::MOTIS->new(
+ log => $self->app->log,
+ main_cache => $self->app->cache_iris_main,
+ realtime_cache => $self->app->cache_iris_rt,
+ root_url => $self->url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->config->{version},
+ );
}
);
@@ -209,6 +208,11 @@ sub startup {
{
return 1;
}
+ if ( ( $self->param('hafas') or $self->param('efa') )
+ and $stop =~ m{ [Bb]ahnhof | Bf }x )
+ {
+ return 1;
+ }
return;
}
);
@@ -218,9 +222,27 @@ sub startup {
my ( $self, $occupancy ) = @_;
my @symbols
- = (qw(help_outline person_outline people priority_high));
+ = (
+ qw(help_outline person_outline people priority_high not_interested)
+ );
my $text = 'Auslastung unbekannt';
+ if ( $occupancy eq 'MANY_SEATS' ) {
+ $occupancy = 1;
+ }
+ elsif ( $occupancy eq 'FEW_SEATS' ) {
+ $occupancy = 2;
+ }
+ elsif ( $occupancy eq 'STANDING_ONLY' ) {
+ $occupancy = 3;
+ }
+ elsif ( $occupancy eq 'FULL' ) {
+ $occupancy = 4;
+ }
+
+ if ( $occupancy > 3 ) {
+ $text = 'Voraussichtlich überfüllt';
+ }
if ( $occupancy > 2 ) {
$text = 'Sehr hohe Auslastung erwartet';
}
@@ -286,6 +308,25 @@ sub startup {
}
);
+ $self->helper(
+ 'get_rt_time_class' => sub {
+ my ( $self, $train ) = @_;
+ if ( $train->{has_realtime}
+ and not $train->{is_bit_delayed}
+ and not $train->{is_delayed} )
+ {
+ return 'on-time';
+ }
+ if ( $train->{is_bit_delayed} ) {
+ return 'a-bit-delayed';
+ }
+ if ( $train->{is_delayed} ) {
+ return 'delayed';
+ }
+ return q{};
+ }
+ );
+
my $r = $self->routes;
$r->get('/_redirect')->to('stationboard#redirect_to_station');
@@ -295,6 +336,8 @@ sub startup {
$r->get('/_autostop')->to('static#geostop');
+ $r->get('/_backend')->to('stationboard#backend_list');
+
$r->get('/_datenschutz')->to('static#privacy');
$r->post('/_geolocation')->to('stationboard#stations_by_coordinates');
@@ -305,25 +348,25 @@ sub startup {
$r->get('/dyn/:av/autocomplete.js')->to('stationboard#autocomplete');
- $r->get('/_wr/:train/:departure')->to('wagenreihung#wagenreihung');
- $r->get('/wr/:train')->to('wagenreihung#zugbildung_db');
+ $r->get('/carriage-formation')->to('wagenreihung#wagenreihung');
$r->get('/w/*wagon')->to('wagenreihung#wagen');
$r->get('/_ajax_mapinfo/:tripid/:lineno')->to('map#ajax_route');
$r->get('/map/:tripid/:lineno')->to('map#route');
- $r->get('/intersection/:trips')->to('map#intersection');
- $r->get( '/z/:train/*station' => 'train_at_station' )
- ->to('stationboard#station_train_details');
- $r->get( '/z/:train' => 'train' )->to('stationboard#train_details');
-
- $r->get('/map')->to('map#search_form');
- $r->get('/_trainsearch')->to('map#search');
+ $r->get('/coverage/:backend/:service')->to('map#coverage');
+ $r->get( '/z/:train/*station' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'stationboard#station_train_details', format => undef )
+ ->name('train_at_station');
+ $r->get( '/z/:train' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'stationboard#train_details', format => undef )
+ ->name('train');
$self->defaults( layout => 'app' );
- $r->get('/')->to('stationboard#handle_request');
- $r->get('/multi/*station')->to('stationboard#handle_request');
- $r->get('/*station')->to('stationboard#handle_request');
+ $r->get('/')->to('stationboard#handle_board_request');
+ $r->get('/multi/*station')->to('stationboard#handle_board_request');
+ $r->get( '/*station' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'stationboard#handle_board_request', format => undef );
$self->types->type( json => 'application/json; charset=utf-8' );
diff --git a/lib/DBInfoscreen/Controller/Map.pm b/lib/DBInfoscreen/Controller/Map.pm
index 93f2b49..0a597e1 100644
--- a/lib/DBInfoscreen/Controller/Map.pm
+++ b/lib/DBInfoscreen/Controller/Map.pm
@@ -1,11 +1,12 @@
package DBInfoscreen::Controller::Map;
# Copyright (C) 2011-2020 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
-use Mojo::JSON qw(decode_json);
+use Mojo::JSON qw(decode_json encode_json);
use Mojo::Promise;
use DateTime;
@@ -19,26 +20,28 @@ my $strp = DateTime::Format::Strptime->new(
);
# Input:
-# - polyline: Travel::Status::DE::HAFAS::Journey->polyline
+# - polyline: [{lat, lon, name?}, ...]
# - from_name: station name
# - to_name: station name
# Ouptut:
-# - from_index: polyline index that corresponds to from_name
-# - to_index: polyline index that corresponds to to_name
+# - from_index: polyline index where name eq from_name
+# - to_index: polyline index where name eq to_name
sub get_route_indexes {
my ( $polyline, $from_name, $to_name ) = @_;
my ( $from_index, $to_index );
for my $i ( 0 .. $#{$polyline} ) {
my $this_point = $polyline->[$i];
+ my $name = $this_point->{name} // $this_point->{stop}->{name};
+
if ( not defined $from_index
- and $this_point->{name}
- and $this_point->{name} eq $from_name )
+ and $name
+ and $name eq $from_name )
{
$from_index = $i;
}
- elsif ( $this_point->{name}
- and $this_point->{name} eq $to_name )
+ elsif ( $name
+ and $name eq $to_name )
{
$to_index = $i;
last;
@@ -50,9 +53,9 @@ sub get_route_indexes {
# Input:
# now: DateTime
# from: current/previous stop
-# {dep => DateTime, name => str, lat => float, lon => float}
+# {arr => DateTime, dep => DateTime, name => str, lat => float, lon => float}
# to: next stop
-# {arr => DateTime, name => str, lat => float, lon => float}
+# {arr => DateTime, dep => DateTime, name => str, lat => float, lon => float}
# route: Travel::Status::DE::HAFAS::Journey->route
# polyline: Travel::Status::DE::HAFAS::Journey->polyline (list of lon/lat hashes)
# Output: list of estimated train positions in [lat, lon] format.
@@ -61,7 +64,7 @@ sub get_route_indexes {
# - position 4 seconds from now
# - ...
sub estimate_train_positions {
- my (%opt) = @_;
+ my ( $self, %opt ) = @_;
my $now = $opt{now};
@@ -135,6 +138,11 @@ sub estimate_train_positions {
}
}
else {
+ $self->log->debug(
+ "Did not find route indexes for $from_name → $to_name");
+ $self->log->debug(
+"Falling back to $opt{from}{lat} $opt{from}{lon} → $opt{to}{lat} $opt{to}{lon}"
+ );
for my $ratio (@completion_ratios) {
my $lat
= $opt{from}{lat} + ( $opt{to}{lat} - $opt{from}{lat} ) * $ratio;
@@ -155,6 +163,8 @@ sub estimate_train_positions {
# name: str
# arr: DateTime
# dep: DateTime
+# arr_delay: int
+# dep_delay: int
# polyline: ref to Travel::Status::DE::HAFAS::Journey polyline list
# Output:
# next_stop: {type, station}
@@ -182,7 +192,7 @@ sub estimate_train_positions2 {
$self->backpropagate_delay( $route[ $i - 1 ], $route[$i] );
# (current position, future positons...) in 2 second steps
- @train_positions = estimate_train_positions(
+ @train_positions = $self->estimate_train_positions(
from => $route[ $i - 1 ],
to => $route[$i],
now => $now,
@@ -234,6 +244,11 @@ sub estimate_train_positions2 {
};
}
+# input: [{
+# name, platform,
+# arr, arr_cancelled, arr_delay,
+# dep, dep_cancelled, dep_delay
+# }]
sub route_to_ajax {
my (@stopovers) = @_;
@@ -245,7 +260,7 @@ sub route_to_ajax {
if ( my $arr = $stop->{arr} and not $stop->{arr_cancelled} ) {
my $delay = $stop->{arr_delay} // 0;
- $platform = $stop->{arr_platform};
+ $platform = $stop->{platform};
push( @stop_entries, $arr->epoch, $delay );
}
@@ -255,7 +270,7 @@ sub route_to_ajax {
if ( my $dep = $stop->{dep} and not $stop->{dep_cancelled} ) {
my $delay = $stop->{dep_delay} // 0;
- $platform //= $stop->{dep_platform} // q{};
+ $platform //= $stop->{platform} // q{};
push( @stop_entries, $dep->epoch, $delay, $platform );
}
@@ -303,17 +318,528 @@ sub backpropagate_delay {
}
}
+sub route_efa {
+ my ($self) = @_;
+ my $trip_id = $self->stash('tripid');
+ my $backend = $self->param('efa');
+
+ my $stopseq;
+ if ( $trip_id
+ =~ m{ ^ ([^@]*) @ ([^@]*) [(] ([^T]*) T ([^)]*) [)] (.*) $ }x )
+ {
+ $stopseq = {
+ stateless => $1,
+ stop_id => $2,
+ date => $3,
+ time => $4,
+ key => $5
+ };
+ }
+ else {
+ $self->render(
+ 'route_map',
+ title => "DBF",
+ hide_opts => 1,
+ with_map => 1,
+ error => "cannot parse trip ID: $trip_id",
+ );
+ return;
+ }
+
+ $self->efa->get_polyline_p(
+ stopseq => $stopseq,
+ service => $backend,
+ )->then(
+ sub {
+ my ($trip) = @_;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my @markers;
+ my @polyline = $trip->polyline( fallback => 1 );
+ my @line_pairs = polyline_to_line_pairs(@polyline);
+ my @route = $trip->route;
+
+ my $ref_route = [
+ map {
+ {
+ name => $_->full_name,
+ platform => $_->platform,
+ arr => $_->arr,
+ dep => $_->dep,
+ arr_delay => $_->arr_delay,
+ dep_delay => $_->dep_delay,
+ lat => $_->latlon->[0],
+ lon => $_->latlon->[1]
+ }
+ } @route
+ ];
+
+ for my $pl (@polyline) {
+ if ( $pl->{stop} ) {
+ $pl->{name} = $pl->{stop}->full_name;
+ }
+ }
+
+ my $train_pos = $self->estimate_train_positions2(
+ now => $now,
+ route => $ref_route,
+ polyline => \@polyline,
+ );
+
+ my @station_coordinates;
+ for my $stop (@route) {
+ my @stop_lines = ( $stop->full_name );
+ if ( $stop->platform ) {
+ push( @stop_lines, 'Gleis ' . $stop->platform );
+ }
+ if ( $stop->arr ) {
+ my $arr_line = $stop->arr->strftime('Ankunft: %H:%M');
+ if ( $stop->arr_delay ) {
+ $arr_line .= sprintf( ' (%+d)', $stop->arr_delay );
+ }
+ push( @stop_lines, $arr_line );
+ }
+ if ( $stop->dep ) {
+ my $dep_line = $stop->dep->strftime('Abfahrt: %H:%M');
+ if ( $stop->dep_delay ) {
+ $dep_line .= sprintf( ' (%+d)', $stop->dep_delay );
+ }
+ push( @stop_lines, $dep_line );
+ }
+
+ push( @station_coordinates, [ $stop->latlon, [@stop_lines], ] );
+ }
+
+ push(
+ @markers,
+ {
+ lat => $train_pos->{position_now}[0],
+ lon => $train_pos->{position_now}[1],
+ title => $trip->name,
+ }
+ );
+
+ $self->render(
+ 'route_map',
+ description => "Karte für " . $trip->name,
+ title => $trip->name,
+ hide_opts => 1,
+ with_map => 1,
+ ajax_req => "${trip_id}/0",
+ ajax_route => route_to_ajax( @{$ref_route} ),
+ ajax_polyline => join( '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ),
+ origin => {
+ name => ( $trip->route )[0]->full_name,
+ ts => ( $trip->route )[0]->dep,
+ },
+ destination => {
+ name => ( $trip->route )[-1]->full_name,
+ ts => ( $trip->route )[-1]->arr,
+ },
+ train_no => $trip->number
+ ? ( $trip->type // q{} . ' ' . $trip->number )
+ : undef,
+ operator => $trip->operator,
+ next_stop => $train_pos->{next_stop},
+ polyline_groups => [
+ {
+ polylines => \@line_pairs,
+ color => '#00838f',
+ opacity => 0.6,
+ fit_bounds => 1,
+ }
+ ],
+ station_coordinates => \@station_coordinates,
+ station_radius => 100,
+ markers => \@markers,
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ 'route_map',
+ title => "DBF",
+ hide_opts => 1,
+ with_map => 1,
+ error => $err,
+ );
+ }
+ )->wait;
+}
+
+sub route_dbris {
+ my ($self) = @_;
+ my $trip_id = $self->stash('tripid');
+
+ my $from_name = $self->param('from');
+ my $to_name = $self->param('to');
+
+ $self->dbris->get_polyline_p( id => $trip_id )->then(
+ sub {
+ my ($journey) = @_;
+
+ my @polyline = $journey->polyline;
+ my @station_coordinates;
+
+ my @markers;
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ # used to draw the train's journey on the map
+ my @line_pairs = polyline_to_line_pairs(@polyline);
+
+ my @route = $journey->route;
+
+ my $train_pos = $self->estimate_train_positions2(
+ now => $now,
+ route => [
+ map {
+ {
+ name => $_->name,
+ arr => $_->arr,
+ dep => $_->dep,
+ arr_delay => $_->arr_delay,
+ dep_delay => $_->dep_delay,
+ lat => $_->lat,
+ lon => $_->lon
+ }
+ } @route
+ ],
+ polyline => \@polyline,
+ );
+
+ # Prepare from/to markers and name/time/delay overlays for stations
+ for my $stop (@route) {
+ my @stop_lines = ( $stop->name );
+
+ if ( $from_name and $stop->name eq $from_name ) {
+ push(
+ @markers,
+ {
+ lon => $stop->lon,
+ lat => $stop->lat,
+ title => $stop->name,
+ icon => 'goldIcon',
+ }
+ );
+ }
+ if ( $to_name and $stop->name eq $to_name ) {
+ push(
+ @markers,
+ {
+ lon => $stop->lon,
+ lat => $stop->lat,
+ title => $stop->name,
+ icon => 'greenIcon',
+ }
+ );
+ }
+
+ if ( $stop->platform ) {
+ push( @stop_lines, 'Gleis ' . $stop->platform );
+ }
+ if ( $stop->arr ) {
+ my $arr_line = $stop->arr->strftime('Ankunft: %H:%M');
+ if ( $stop->arr_delay ) {
+ $arr_line .= sprintf( ' (%+d)', $stop->arr_delay );
+ }
+ push( @stop_lines, $arr_line );
+ }
+ if ( $stop->dep ) {
+ my $dep_line = $stop->dep->strftime('Abfahrt: %H:%M');
+ if ( $stop->dep_delay ) {
+ $dep_line .= sprintf( ' (%+d)', $stop->dep_delay );
+ }
+ push( @stop_lines, $dep_line );
+ }
+
+ push( @station_coordinates,
+ [ [ $stop->lat, $stop->lon ], [@stop_lines], ] );
+ }
+
+ push(
+ @markers,
+ {
+ lat => $train_pos->{position_now}[0],
+ lon => $train_pos->{position_now}[1],
+ title => $journey->train,
+ }
+ );
+
+ $self->render(
+ 'route_map',
+ description => "Karte für " . $journey->train,
+ title => $journey->train,
+ hide_opts => 1,
+ with_map => 1,
+ ajax_req => "${trip_id}/0",
+ ajax_route => route_to_ajax(
+ map {
+ {
+ name => $_->name,
+ platform => $_->platform,
+ arr => $_->arr,
+ arr_cancelled => $_->is_cancelled,
+ arr_delay => $_->arr_delay,
+ dep => $_->dep,
+ dep_cancelled => $_->is_cancelled,
+ dep_delay => $_->dep_delay,
+ }
+ } $journey->route
+ ),
+ ajax_polyline => join(
+ '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} }
+ ),
+ origin => {
+ name => ( $journey->route )[0]->name,
+ ts => ( $journey->route )[0]->dep,
+ },
+ destination => {
+ name => ( $journey->route )[-1]->name,
+ ts => ( $journey->route )[-1]->arr,
+ },
+ train_no => $journey->number
+ ? ( $journey->type // q{} . ' ' . $journey->number )
+ : undef,
+ next_stop => $train_pos->{next_stop},
+ polyline_groups => [
+ {
+ polylines => [@line_pairs],
+ color => '#00838f',
+ opacity => 0.6,
+ fit_bounds => 1,
+ }
+ ],
+ station_coordinates => [@station_coordinates],
+ station_radius =>
+ ( $train_pos->{avg_inter_stop_beeline} > 500 ? 250 : 100 ),
+ markers => [@markers],
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ 'route_map',
+ title => "DBF",
+ hide_opts => 1,
+ with_map => 1,
+ error => $err,
+ );
+
+ }
+ )->wait;
+}
+
+sub route_motis {
+ my ($self) = @_;
+
+ my $service = $self->param('motis') // 'transitous';
+ my $trip_id = $self->stash('tripid');
+
+ my $from_name = $self->param('from');
+ my $to_name = $self->param('to');
+
+ $self->motis->get_polyline_p(
+ service => $service,
+ id => $trip_id,
+ )->then(
+ sub {
+ my ($trip) = @_;
+
+ my @polyline = $trip->polyline;
+ my @station_coordinates;
+
+ my @markers;
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ # used to draw the train's journey on the map
+ my @line_pairs = polyline_to_line_pairs(@polyline);
+
+ my @stopovers = $trip->stopovers;
+
+ my $train_pos = $self->estimate_train_positions2(
+ now => $now,
+ route => [
+ map {
+ {
+ name => $_->stop->name,
+ arr => $_->arrival,
+ dep => $_->departure,
+ arr_delay => $_->arrival_delay,
+ dep_delay => $_->departure_delay,
+ lat => $_->stop->lat,
+ lon => $_->stop->lon,
+ }
+ } @stopovers
+ ],
+ polyline => \@polyline,
+ );
+
+ # Prepare from/to markers and name/time/delay overlays for stations
+ for my $stopover (@stopovers) {
+ my $stop = $stopover->stop;
+ my @stop_lines = ( $stop->name );
+
+ if ( $from_name and $stop->name eq $from_name ) {
+ push(
+ @markers,
+ {
+ lon => $stop->lon,
+ lat => $stop->lat,
+ title => $stop->name,
+ icon => 'goldIcon',
+ }
+ );
+ }
+ if ( $to_name and $stop->name eq $to_name ) {
+ push(
+ @markers,
+ {
+ lon => $stop->lon,
+ lat => $stop->lat,
+ title => $stop->name,
+ icon => 'greenIcon',
+ }
+ );
+ }
+
+ if ( $stopover->track ) {
+ push( @stop_lines, 'Gleis ' . $stop->track );
+ }
+ if ( $stopover->arrival ) {
+ my $arr_line
+ = $stopover->arrival->strftime('Ankunft: %H:%M');
+ if ( $stopover->arrival_delay ) {
+ $arr_line
+ .= sprintf( ' (%+d)', $stopover->arrival_delay );
+ }
+ push( @stop_lines, $arr_line );
+ }
+ if ( $stopover->departure ) {
+ my $dep_line
+ = $stopover->departure->strftime('Abfahrt: %H:%M');
+ if ( $stopover->departure_delay ) {
+ $dep_line
+ .= sprintf( ' (%+d)', $stopover->departure_delay );
+ }
+ push( @stop_lines, $dep_line );
+ }
+
+ push( @station_coordinates,
+ [ [ $stop->lat, $stop->lon ], [@stop_lines], ] );
+ }
+
+ push(
+ @markers,
+ {
+ lat => $train_pos->{position_now}[0],
+ lon => $train_pos->{position_now}[1],
+ title => $trip->route_name,
+ }
+ );
+
+ $self->render(
+ 'route_map',
+ description => "Karte für " . $trip->route_name,
+ title => $trip->route_name,
+ hide_opts => 1,
+ with_map => 1,
+ ajax_req => "${trip_id}/0",
+ ajax_route => route_to_ajax(
+ map {
+ {
+ name => $_->stop->name,
+ platform => $_->track,
+ arr => $_->arrival,
+ arr_cancelled => $_->is_cancelled,
+ arr_delay => $_->arrival_delay,
+ dep => $_->departure,
+ dep_cancelled => $_->is_cancelled,
+ dep_delay => $_->departure_delay,
+ }
+ } $trip->stopovers
+ ),
+ ajax_polyline => join(
+ '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} }
+ ),
+ origin => {
+ name => ( $trip->stopovers )[0]->stop->name,
+ ts => ( $trip->stopovers )[0]->departure,
+ },
+ destination => {
+ name => ( $trip->stopovers )[-1]->stop->name,
+ ts => ( $trip->stopovers )[-1]->arrival,
+ },
+ train_no => undef, # FIXME: Better value?
+ next_stop => $train_pos->{next_stop},
+ polyline_groups => [
+ {
+ polylines => [@line_pairs],
+ color => '#00838f',
+ opacity => 0.6,
+ fit_bounds => 1,
+ }
+ ],
+ station_coordinates => [@station_coordinates],
+ station_radius =>
+ ( $train_pos->{avg_inter_stop_beeline} > 500 ? 250 : 100 ),
+ markers => [@markers],
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ 'route_map',
+ title => "DBF",
+ hide_opts => 1,
+ with_map => 1,
+ error => $err,
+ );
+
+ }
+ )->wait;
+}
+
sub route {
my ($self) = @_;
my $trip_id = $self->stash('tripid');
my $line_no = $self->stash('lineno');
+ my $hafas = $self->param('hafas');
my $from_name = $self->param('from');
my $to_name = $self->param('to');
$self->render_later;
- $self->hafas->get_polyline_p( $trip_id, $line_no )->then(
+ if ( $self->param('dbris') ) {
+ return $self->route_dbris;
+ }
+ if ( $self->param('motis') ) {
+ return $self->route_motis;
+ }
+ if ( $self->param('efa') ) {
+ return $self->route_efa;
+ }
+
+ my $service = 'ÖBB';
+ if ( $hafas
+ and $hafas ne '1'
+ and Travel::Status::DE::HAFAS::get_service($hafas) )
+ {
+ $service = $hafas;
+ }
+
+ $self->hafas->get_polyline_p(
+ id => $trip_id,
+ line => $line_no,
+ service => $service
+ )->then(
sub {
my ($journey) = @_;
@@ -321,7 +847,6 @@ sub route {
my @station_coordinates;
my @markers;
- my $next_stop;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
@@ -331,58 +856,70 @@ sub route {
my @route = $journey->route;
my $train_pos = $self->estimate_train_positions2(
- now => $now,
- route => \@route,
+ now => $now,
+ route => [
+ map {
+ {
+ name => $_->loc->name,
+ arr => $_->arr,
+ dep => $_->dep,
+ arr_delay => $_->arr_delay,
+ dep_delay => $_->dep_delay,
+ lat => $_->loc->lat,
+ lon => $_->loc->lon
+ }
+ } @route
+ ],
polyline => \@polyline,
);
# Prepare from/to markers and name/time/delay overlays for stations
for my $stop (@route) {
- my @stop_lines = ( $stop->{name} );
+ my @stop_lines = ( $stop->loc->name );
- if ( $from_name and $stop->{name} eq $from_name ) {
+ if ( $from_name and $stop->loc->name eq $from_name ) {
push(
@markers,
{
- lon => $stop->{lon},
- lat => $stop->{lat},
- title => $stop->{name},
+ lon => $stop->loc->lon,
+ lat => $stop->loc->lat,
+ title => $stop->loc->name,
icon => 'goldIcon',
}
);
}
- if ( $to_name and $stop->{name} eq $to_name ) {
+ if ( $to_name and $stop->loc->name eq $to_name ) {
push(
@markers,
{
- lon => $stop->{lon},
- lat => $stop->{lat},
- title => $stop->{name},
+ lon => $stop->loc->lon,
+ lat => $stop->loc->lat,
+ title => $stop->loc->name,
icon => 'greenIcon',
}
);
}
- if ( $stop->{platform} ) {
- push( @stop_lines, 'Gleis ' . $stop->{platform} );
+ if ( $stop->platform ) {
+ push( @stop_lines, 'Gleis ' . $stop->platform );
}
- if ( $stop->{arr} ) {
- my $arr_line = $stop->{arr}->strftime('Ankunft: %H:%M');
- if ( $stop->{arr_delay} ) {
- $arr_line .= sprintf( ' (%+d)', $stop->{arr_delay} );
+ if ( $stop->arr ) {
+ my $arr_line = $stop->arr->strftime('Ankunft: %H:%M');
+ if ( $stop->arr_delay ) {
+ $arr_line .= sprintf( ' (%+d)', $stop->arr_delay );
}
push( @stop_lines, $arr_line );
}
- if ( $stop->{dep} ) {
- my $dep_line = $stop->{dep}->strftime('Abfahrt: %H:%M');
- if ( $stop->{dep_delay} ) {
- $dep_line .= sprintf( ' (%+d)', $stop->{dep_delay} );
+ if ( $stop->dep ) {
+ my $dep_line = $stop->dep->strftime('Abfahrt: %H:%M');
+ if ( $stop->dep_delay ) {
+ $dep_line .= sprintf( ' (%+d)', $stop->dep_delay );
}
push( @stop_lines, $dep_line );
}
push( @station_coordinates,
- [ [ $stop->{lat}, $stop->{lon} ], [@stop_lines], ] );
+ [ [ $stop->loc->lat, $stop->loc->lon ], [@stop_lines], ] );
}
push(
@@ -393,31 +930,45 @@ sub route {
title => $journey->name
}
);
- $next_stop = $train_pos->{next_stop};
$self->render(
'route_map',
- description => "Karte für " . $journey->name,
- title => $journey->name,
- hide_opts => 1,
- with_map => 1,
- ajax_req => "${trip_id}/${line_no}",
- ajax_route => route_to_ajax( $journey->route ),
- ajax_polyline => join( '|',
- map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ),
+ description => "Karte für " . $journey->name,
+ title => $journey->name,
+ hide_opts => 1,
+ with_map => 1,
+ ajax_req => "${trip_id}/${line_no}",
+ ajax_route => route_to_ajax(
+ map {
+ {
+ name => $_->loc->name,
+ platform => $_->platform,
+ arr => $_->arr,
+ arr_cancelled => $_->arr_cancelled,
+ arr_delay => $_->arr_delay,
+ dep => $_->dep,
+ dep_cancelled => $_->dep_cancelled,
+ dep_delay => $_->dep_delay,
+ }
+ } $journey->route
+ ),
+ ajax_polyline => join(
+ '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} }
+ ),
origin => {
- name => ( $journey->route )[0]->{name},
- ts => ( $journey->route )[0]->{dep},
+ name => ( $journey->route )[0]->loc->name,
+ ts => ( $journey->route )[0]->dep,
},
destination => {
name => $journey->route_end,
- ts => ( $journey->route )[-1]->{arr},
+ ts => ( $journey->route )[-1]->arr,
},
train_no => $journey->number
- ? ( $journey->type . ' ' . $journey->number )
+ ? ( $journey->type // q{} . ' ' . $journey->number )
: undef,
operator => $journey->operator,
- next_stop => $next_stop,
+ next_stop => $train_pos->{next_stop},
polyline_groups => [
{
polylines => [@line_pairs],
@@ -447,16 +998,108 @@ sub route {
)->wait;
}
-sub ajax_route {
+sub ajax_route_efa {
my ($self) = @_;
+ my $backend = $self->param('efa');
my $trip_id = $self->stash('tripid');
- my $line_no = $self->stash('lineno');
- delete $self->stash->{layout};
+ my $stopseq;
+ if ( $trip_id
+ =~ m{ ^ ([^@]*) @ ([^@]*) [(] ([^T]*) T ([^)]*) [)] (.*) $ }x )
+ {
+ $stopseq = {
+ stateless => $1,
+ stop_id => $2,
+ date => $3,
+ time => $4,
+ key => $5
+ };
+ }
+ else {
+ $self->render(
+ '_error',
+ error => "cannot parse trip ID: $trip_id",
+ );
+ return;
+ }
- $self->render_later;
+ $self->efa->get_polyline_p(
+ stopseq => $stopseq,
+ service => $backend
+ )->then(
+ sub {
+ my ($trip) = @_;
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ my @polyline = $trip->polyline( fallback => 1 );
+ my @route = $trip->route;
+
+ my $ref_route = [
+ map {
+ {
+ name => $_->full_name,
+ platform => $_->platform,
+ arr => $_->arr,
+ dep => $_->dep,
+ arr_delay => $_->arr_delay,
+ dep_delay => $_->dep_delay,
+ lat => $_->latlon->[0],
+ lon => $_->latlon->[1]
+ }
+ } @route
+ ];
+
+ for my $pl (@polyline) {
+ if ( $pl->{stop} ) {
+ $pl->{name} = $pl->{stop}->full_name;
+ }
+ }
+
+ my $train_pos = $self->estimate_train_positions2(
+ now => $now,
+ route => $ref_route,
+ polyline => \@polyline,
+ );
+
+ $self->render(
+ '_map_infobox',
+ ajax_req => "${trip_id}/0",
+ ajax_route => route_to_ajax( @{$ref_route} ),
+ ajax_polyline => join( '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ),
+ origin => {
+ name => ( $trip->route )[0]->full_name,
+ ts => ( $trip->route )[0]->dep,
+ },
+ destination => {
+ name => ( $trip->route )[-1]->full_name,
+ ts => ( $trip->route )[-1]->arr,
+ },
+ train_no => $trip->number
+ ? ( $trip->type // q{} . ' ' . $trip->number )
+ : undef,
+ next_stop => $train_pos->{next_stop},
+ );
+ }
+ )->catch(
+ sub {
+ sub {
+ my ($err) = @_;
+ $self->render(
+ '_error',
+ error => $err,
+ );
+ }
+ }
+ )->wait;
+}
- $self->hafas->get_polyline_p( $trip_id, $line_no )->then(
+sub ajax_route_dbris {
+ my ($self) = @_;
+ my $trip_id = $self->stash('tripid');
+
+ $self->dbris->get_polyline_p( id => $trip_id )->then(
sub {
my ($journey) = @_;
@@ -466,29 +1109,57 @@ sub ajax_route {
my @polyline = $journey->polyline;
my $train_pos = $self->estimate_train_positions2(
- now => $now,
- route => \@route,
+ now => $now,
+ route => [
+ map {
+ {
+ name => $_->name,
+ arr => $_->arr,
+ dep => $_->dep,
+ arr_delay => $_->arr_delay,
+ dep_delay => $_->dep_delay,
+ lat => $_->lat,
+ lon => $_->lon
+ }
+ } @route
+ ],
polyline => \@polyline,
);
$self->render(
'_map_infobox',
- ajax_req => "${trip_id}/${line_no}",
- ajax_route => route_to_ajax(@route),
- ajax_polyline => join( '|',
- map { join( ';', @{$_} ) } @{ $train_pos->{positions} } ),
+ ajax_req => "${trip_id}/0",
+ ajax_route => route_to_ajax(
+ map {
+ {
+ name => $_->name,
+ platform => $_->platform,
+ arr => $_->arr,
+ arr_cancelled => $_->is_cancelled,
+ arr_delay => $_->arr_delay,
+ dep => $_->dep,
+ dep_cancelled => $_->is_cancelled,
+ dep_delay => $_->dep_delay,
+ }
+ } @route
+ ),
+ ajax_polyline => join(
+ '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} }
+ ),
origin => {
- name => ( $journey->route )[0]->{name},
- ts => ( $journey->route )[0]->{dep},
+ name => ( $journey->route )[0]->name,
+ ts => ( $journey->route )[0]->dep,
},
destination => {
- name => $journey->route_end,
- ts => ( $journey->route )[-1]->{arr},
+ name => ( $journey->route )[-1]->name,
+ ts => ( $journey->route )[-1]->arr,
},
train_no => $journey->number
? ( $journey->type . ' ' . $journey->number )
: undef,
- next_stop => $train_pos->{next_stop},
+ next_stop => $train_pos->{next_stop},
+ platform_type => q{},
);
}
)->catch(
@@ -502,77 +1173,216 @@ sub ajax_route {
)->wait;
}
-sub search {
+sub ajax_route_motis {
my ($self) = @_;
- my $t1 = $self->param('train1');
- my $t2 = $self->param('train2');
+ my $service = $self->param('motis') // 'transitous';
+ my $trip_id = $self->stash('tripid');
- my $t1_data;
- my $t2_data;
+ $self->motis->get_polyline_p(
+ service => $service,
+ id => $trip_id,
+ )->then(
+ sub {
+ my ($trip) = @_;
- my @requests;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- if ( not( $t1 and $t1 =~ m{^\S+\s+\d+$} )
- or ( $t2 and not $t2 =~ m{^\S+\s+\d+$} ) )
- {
- $self->render(
- 'trainsearch',
- title => 'Fahrtverlauf',
- hide_opts => 1,
- error => $t1
- ? "Züge müssen im Format 'Zugtyp Nummer' angegeben werden, z.B. 'RE 1234'"
- : undef,
- );
- return;
- }
+ my @stopovers = $trip->stopovers;
+ my @polyline = $trip->polyline;
+
+ my $train_pos = $self->estimate_train_positions2(
+ now => $now,
+ route => [
+ map {
+ {
+ name => $_->stop->name,
+ arr => $_->arrival,
+ dep => $_->departure,
+ arr_delay => $_->arrival_delay,
+ dep_delay => $_->departure_delay,
+ lat => $_->stop->lat,
+ lon => $_->stop->lon,
+ }
+ } @stopovers
+ ],
+ polyline => \@polyline,
+ );
+
+ $self->render(
+ '_map_infobox',
+ ajax_req => "${trip_id}/0",
+ ajax_route => route_to_ajax(
+ map {
+ {
+ name => $_->stop->name,
+ platform => $_->track,
+ arr => $_->arrival,
+ arr_cancelled => $_->is_cancelled,
+ arr_delay => $_->arrival_delay,
+ dep => $_->departure,
+ dep_cancelled => $_->is_cancelled,
+ dep_delay => $_->departure_delay,
+ }
+ } @stopovers
+ ),
+ ajax_polyline => join(
+ '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} }
+ ),
+ origin => {
+ name => ( $trip->stopovers )[0]->stop->name,
+ ts => ( $trip->stopovers )[0]->departure,
+ },
+ destination => {
+ name => ( $trip->stopovers )[-1]->stop->name,
+ ts => ( $trip->stopovers )[-1]->arrival,
+ },
+ train_no => undef, # FIXME
+ next_stop => $train_pos->{next_stop},
+ platform_type => q{},
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ '_error',
+ error => $err,
+ );
+ }
+ )->wait;
+}
+
+sub ajax_route {
+ my ($self) = @_;
+
+ delete $self->stash->{layout};
$self->render_later;
- push( @requests, $self->hafas->trainsearch_p( train_no => $t1 ) );
+ if ( $self->param('dbris') ) {
+ return $self->ajax_route_dbris;
+ }
+ if ( $self->param('motis') ) {
+ return $self->ajax_route_motis;
+ }
+ if ( $self->param('efa') ) {
+ return $self->ajax_route_efa;
+ }
+
+ my $trip_id = $self->stash('tripid');
+ my $line_no = $self->stash('lineno');
+ my $hafas = $self->param('hafas');
- if ($t2) {
- push( @requests, $self->hafas->trainsearch_p( train_no => $t2 ) );
+ my $service = 'ÖBB';
+ if ( $hafas
+ and $hafas ne '1'
+ and Travel::Status::DE::HAFAS::get_service($hafas) )
+ {
+ $service = $hafas;
}
- Mojo::Promise->all(@requests)->then(
+ $self->hafas->get_polyline_p(
+ id => $trip_id,
+ line => $line_no,
+ service => $service
+ )->then(
sub {
- my ( $t1_data, $t2_data ) = @_;
-
- if ($t2_data) {
- $self->redirect_to(
- sprintf(
- "/intersection/%s,0;%s,0",
- $t1_data->[0]{trip_id},
- $t2_data->[0]{trip_id},
- )
- );
- }
- else {
- $self->redirect_to(
- sprintf( "/map/%s/0", $t1_data->[0]{trip_id}, ) );
- }
+ my ($journey) = @_;
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+
+ my @route = $journey->route;
+ my @polyline = $journey->polyline;
+
+ my $train_pos = $self->estimate_train_positions2(
+ now => $now,
+ route => [
+ map {
+ {
+ name => $_->loc->name,
+ arr => $_->arr,
+ dep => $_->dep,
+ arr_delay => $_->arr_delay,
+ dep_delay => $_->dep_delay,
+ lat => $_->loc->lat,
+ lon => $_->loc->lon
+ }
+ } @route
+ ],
+ polyline => \@polyline,
+ );
+
+ $self->render(
+ '_map_infobox',
+ ajax_req => "${trip_id}/${line_no}",
+ ajax_route => route_to_ajax(
+ map {
+ {
+ name => $_->loc->name,
+ platform => $_->platform,
+ arr => $_->arr,
+ arr_cancelled => $_->arr_cancelled,
+ arr_delay => $_->arr_delay,
+ dep => $_->dep,
+ dep_cancelled => $_->dep_cancelled,
+ dep_delay => $_->dep_delay,
+ }
+ } @route
+ ),
+ ajax_polyline => join(
+ '|',
+ map { join( ';', @{$_} ) } @{ $train_pos->{positions} }
+ ),
+ origin => {
+ name => ( $journey->route )[0]->loc->name,
+ ts => ( $journey->route )[0]->dep,
+ },
+ destination => {
+ name => $journey->route_end,
+ ts => ( $journey->route )[-1]->arr,
+ },
+ train_no => $journey->number
+ ? ( $journey->type . ' ' . $journey->number )
+ : undef,
+ next_stop => $train_pos->{next_stop},
+ );
}
)->catch(
sub {
my ($err) = @_;
$self->render(
- 'trainsearch',
- title => 'Fahrtverlauf',
- hide_opts => 1,
- error => $err
+ '_error',
+ error => $err,
);
}
)->wait;
}
-sub search_form {
- my ($self) = @_;
+sub coverage {
+ my ($self) = @_;
+ my $backend = lc( $self->stash('backend') );
+ my $service = $self->stash('service');
+
+ my $coverage = {};
+
+ if ( $backend eq 'efa' ) {
+ $coverage = $self->efa->get_coverage($service);
+ }
+ elsif ( $backend eq 'hafas' ) {
+ $coverage = $self->hafas->get_coverage($service);
+ }
+ elsif ( $backend eq 'motis' ) {
+ $coverage = $self->motis->get_coverage($service);
+ }
$self->render(
- 'trainsearch',
- title => 'Fahrtverlauf',
+ 'coverage_map',
+ title => "Abdeckung $service",
hide_opts => 1,
+ with_map => 1,
+ coverage => encode_json($coverage),
);
}
diff --git a/lib/DBInfoscreen/Controller/Static.pm b/lib/DBInfoscreen/Controller/Static.pm
index 927bf6e..9a57f05 100644
--- a/lib/DBInfoscreen/Controller/Static.pm
+++ b/lib/DBInfoscreen/Controller/Static.pm
@@ -17,7 +17,8 @@ sub geostop {
$self->render(
'geostop',
with_geostop => 1,
- hide_opts => 1
+ hide_opts => 1,
+ hide_footer => 1,
);
}
@@ -27,19 +28,20 @@ sub about {
$self->render(
'about',
hide_opts => 1,
+ hide_footer => 1,
);
}
sub privacy {
my ($self) = @_;
- $self->render( 'privacy', hide_opts => 1 );
+ $self->render( 'privacy', hide_opts => 1, hide_footer => 1 );
}
sub imprint {
my ($self) = @_;
- $self->render( 'imprint', hide_opts => 1 );
+ $self->render( 'imprint', hide_opts => 1, hide_footer => 1 );
}
1;
diff --git a/lib/DBInfoscreen/Controller/Stationboard.pm b/lib/DBInfoscreen/Controller/Stationboard.pm
index 6d75d2c..3e07f90 100644
--- a/lib/DBInfoscreen/Controller/Stationboard.pm
+++ b/lib/DBInfoscreen/Controller/Stationboard.pm
@@ -16,6 +16,9 @@ use List::MoreUtils qw();
use Mojo::JSON qw(decode_json encode_json);
use Mojo::Promise;
use Mojo::UserAgent;
+use Travel::Status::DE::DBRIS;
+use Travel::Status::DE::DBRIS::Formation;
+use Travel::Status::DE::EFA;
use Travel::Status::DE::HAFAS;
use Travel::Status::DE::IRIS;
use Travel::Status::DE::IRIS::Stations;
@@ -28,17 +31,56 @@ my %default = (
admode => 'deparr',
);
+sub class_to_product {
+ my ( $self, $hafas ) = @_;
+
+ my $bits = $hafas->get_active_service->{productbits};
+ my $ret;
+
+ for my $i ( 0 .. $#{$bits} ) {
+ $ret->{ 2**$i }
+ = ref( $bits->[$i] ) eq 'ARRAY' ? $bits->[$i][0] : $bits->[$i];
+ }
+
+ return $ret;
+}
+
sub handle_no_results {
- my ( $self, $station, $data, $hafas ) = @_;
+ my ( $self, $station, $data, $hafas, $efa ) = @_;
my $errstr = $data->{errstr};
- if ($hafas) {
+ if ($efa) {
+ if ( $errstr =~ m{ambiguous} and $efa->name_candidates ) {
+ $self->render(
+ 'landingpage',
+ stationlist => [ $efa->name_candidates ],
+ hide_opts => 0,
+ status => $data->{status} // 300,
+ );
+ }
+ else {
+ $self->render(
+ 'landingpage',
+ error => ( $errstr // "Keine Abfahrten an '$station'" ),
+ hide_opts => 0,
+ status => $data->{status} // 404,
+ );
+ }
+ return;
+ }
+ elsif ($hafas) {
$self->render_later;
+ my $service = 'ÖBB';
+ if ( $hafas ne '1' and Travel::Status::DE::HAFAS::get_service($hafas) )
+ {
+ $service = $hafas;
+ }
Travel::Status::DE::HAFAS->new_p(
locationSearch => $station,
+ service => $service,
promise => 'Mojo::Promise',
- user_agent => $self->ua,
+ user_agent => $service eq 'PKP' ? Mojo::UserAgent->new : $self->ua,
)->then(
sub {
my ($status) = @_;
@@ -210,8 +252,17 @@ sub result_has_train_type {
sub result_has_via {
my ( $result, $via ) = @_;
- my @route = $result->route_post;
+ my @route;
+ if ( $result->isa('Travel::Status::DE::IRIS::Result') ) {
+ @route = ( $result->route_post, $result->sched_route_post );
+ }
+ elsif ( $result->isa('Travel::Status::DE::HAFAS::Journey') ) {
+ @route = map { $_->loc->name } $result->route;
+ }
+ elsif ( $result->isa('Travel::Status::DE::EFA::Departure') ) {
+ @route = map { $_->full_name } $result->route_post;
+ }
my $eq_result = List::MoreUtils::any { lc eq lc($via) } @route;
if ($eq_result) {
@@ -320,8 +371,76 @@ sub get_results_p {
my ( $self, $station, %opt ) = @_;
my $data;
+ if ( $opt{dbris} ) {
+ if ( $station =~ m{ [@] L = (?<eva> \d+ ) [@] }x ) {
+ return Travel::Status::DE::DBRIS->new_p(
+ station => {
+ eva => $+{eva},
+ id => $station,
+ },
+ cache => $opt{cache_iris_rt},
+ lwp_options => {
+ timeout => 10,
+ agent => 'dbf.finalrewind.org/2'
+ },
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ );
+ }
+ my $promise = Mojo::Promise->new;
+ Travel::Status::DE::DBRIS->new_p(
+ locationSearch => $station,
+ cache => $opt{cache_iris_main},
+ lwp_options => {
+ timeout => 10,
+ agent => 'dbf.finalrewind.org/2'
+ },
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ )->then(
+ sub {
+ my ($dbris) = @_;
+ $promise->reject( 'station disambiguation', $dbris );
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject("'$err' while trying to look up '$station'");
+ return;
+ }
+ )->wait;
+ return $promise;
+ }
+ if ( $opt{efa} ) {
+ my $service = 'VRR';
+ if ( $opt{efa} ne '1'
+ and Travel::Status::DE::EFA::get_service( $opt{efa} ) )
+ {
+ $service = $opt{efa};
+ }
+ return Travel::Status::DE::EFA->new_p(
+ service => $service,
+ name => $station,
+ full_routes => 1,
+ cache => $opt{cache_iris_rt},
+ lwp_options => {
+ timeout => 10,
+ agent => 'dbf.finalrewind.org/2'
+ },
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ );
+ }
if ( $opt{hafas} ) {
+ my $service = 'ÖBB';
+ if ( $opt{hafas} ne '1'
+ and Travel::Status::DE::HAFAS::get_service( $opt{hafas} ) )
+ {
+ $service = $opt{hafas};
+ }
return Travel::Status::DE::HAFAS->new_p(
+ service => $service,
station => $station,
arrivals => $opt{arrivals},
cache => $opt{cache_iris_rt},
@@ -330,7 +449,7 @@ sub get_results_p {
agent => 'dbf.finalrewind.org/2'
},
promise => 'Mojo::Promise',
- user_agent => $self->ua,
+ user_agent => $service eq 'PKP' ? Mojo::UserAgent->new : $self->ua,
);
}
@@ -377,17 +496,21 @@ sub get_results_p {
}
}
-sub handle_request {
+sub handle_board_request {
my ($self) = @_;
my $station = $self->stash('station');
my $template = $self->param('mode') // 'app';
- my $hafas = !!$self->param('hafas');
+ my $dbris = $self->param('dbris');
+ my $efa = $self->param('efa');
+ my $hafas = $self->param('hafas');
my $with_related = !$self->param('no_related');
my %opt = (
cache_iris_main => $self->app->cache_iris_main,
cache_iris_rt => $self->app->cache_iris_rt,
lookahead => $self->config->{lookahead},
+ dbris => $dbris,
+ efa => $efa,
hafas => $hafas,
);
@@ -439,13 +562,19 @@ sub handle_request {
# (or used by) marudor.de, it was renamed to 'json'. Many clients won't
# notice this for year to come, so we make sure mode=marudor still works as
# intended.
- if ( $template eq 'marudor' ) {
+ if (
+ $template eq 'marudor'
+ or ( $self->req->headers->accept
+ and $self->req->headers->accept eq 'application/json' )
+ )
+ {
$template = 'json';
}
$self->param( mode => $template );
if ( not $station ) {
+ $self->param( rt => 1 );
$self->render( 'landingpage', show_intro => 1 );
return;
}
@@ -460,8 +589,8 @@ sub handle_request {
if ( $self->param('train') and not $opt{datetime} ) {
- # request results from twenty minutes ago to avoid train details suddenly
- # becoming unavailable when its scheduled departure is reached.
+ # request results from twenty minutes ago to avoid train details suddenly
+ # becoming unavailable when its scheduled departure is reached.
$opt{datetime} = DateTime->now( time_zone => 'Europe/Berlin' )
->subtract( minutes => 20 );
$opt{lookahead} = $self->config->{lookahead} + 20;
@@ -472,8 +601,17 @@ sub handle_request {
$self->get_results_p( $station, %opt )->then(
sub {
my ($status) = @_;
+ if ($dbris) {
+ $self->render_board_dbris( $station, $status );
+ return;
+ }
+ if ($efa) {
+ $self->render_board_efa( $station, $status );
+ return;
+ }
my $data = {
results => [ $status->results ],
+ hafas => $hafas ? $status : undef,
station_ds100 =>
( $status->station ? $status->station->{ds100} : undef ),
station_eva => (
@@ -487,12 +625,6 @@ sub handle_request {
( $status->station ? $status->station->{name} : $station ),
};
- if ( $status->station and $status->station->{names} ) {
- $data->{station_name}
- = List::Util::reduce { length($a) < length($b) ? $a : $b }
- @{ $status->station->{names} };
- }
-
if ( not @{ $data->{results} } and $template eq 'json' ) {
$self->handle_no_results_json( $station, $data, $api_version );
return;
@@ -501,17 +633,27 @@ sub handle_request {
$self->handle_no_results( $station, $data, $hafas );
return;
}
- $self->handle_result($data);
+ $self->render_board_hafas($data);
}
)->catch(
sub {
- my ($err) = @_;
+ my ( $err, $status ) = @_;
+ if ( $dbris and $err eq 'station disambiguation' ) {
+ for my $result ( $status->results ) {
+ if ( defined $result->eva ) {
+ $self->redirect_to(
+ '/' . $result->id . '?dbris=bahn.de' );
+ return;
+ }
+ }
+ }
if ( $template eq 'json' ) {
$self->handle_no_results_json(
$station,
{
errstr => $err,
- status => ( $err =~ m{Ambiguous|LOCATION} ? 300 : 500 ),
+ status =>
+ ( $err =~ m{[Aa]mbiguous|LOCATION} ? 300 : 500 ),
},
$api_version
);
@@ -521,9 +663,10 @@ sub handle_request {
$station,
{
errstr => $err,
- status => ( $err =~ m{Ambiguous|LOCATION} ? 300 : 500 ),
+ status => ( $err =~ m{[Aa]mbiguous|LOCATION} ? 300 : 500 ),
},
- $hafas
+ $hafas,
+ $efa ? $status : undef
);
return;
}
@@ -582,7 +725,7 @@ sub format_iris_result_info {
$info .= ": ${delaymsg}";
}
}
- elsif ( $result->delay and $result->delay > 0 ) {
+ elsif ( $result->delay and $result->delay >= 20 ) {
if ( $template eq 'app' or $template eq 'infoscreen' ) {
$info = $delaymsg;
}
@@ -708,15 +851,88 @@ sub render_train {
my @requests
= ( $wagonorder_req, $occupancy_req, $stationinfo_req, $route_req );
- if ( $departure->{wr_link} ) {
- $self->wagonorder->is_available_p( $result, $departure->{wr_link} )
- ->then(
+ if ( $departure->{wr_dt} ) {
+ $self->wagonorder->get_p(
+ train_type => $result->type,
+ train_number => $result->train_no,
+ datetime => $departure->{wr_dt},
+ eva => $departure->{eva}
+ )->then(
sub {
- # great!
+ my ( $wr_json, $wr_param ) = @_;
+ eval {
+ my $wr
+ = Travel::Status::DE::DBRIS::Formation->new(
+ json => $wr_json );
+ $departure->{wr} = $wr;
+ $departure->{wr_link} = join( '&',
+ map { $_ . '=' . $wr_param->{$_} } keys %{$wr_param} );
+ $departure->{wr_text} = join( q{ • },
+ map { $_->desc_short }
+ grep { $_->desc_short } $wr->groups );
+ my $first = 0;
+ for my $group ( $wr->groups ) {
+ my $had_entry = 0;
+ for my $wagon ( $group->carriages ) {
+ if (
+ not( $wagon->is_locomotive
+ or $wagon->is_powercar )
+ )
+ {
+ my $class;
+ if ($first) {
+ push(
+ @{ $departure->{wr_preview} },
+ [ '•', 'meta' ]
+ );
+ $first = 0;
+ }
+ my $entry;
+ if ( $wagon->is_closed ) {
+ $entry = 'X';
+ $class = 'closed';
+ }
+ elsif ( $wagon->number ) {
+ $entry = $wagon->number;
+ }
+ else {
+ if ( $wagon->has_first_class ) {
+ if ( $wagon->has_second_class ) {
+ $entry = '½';
+ }
+ else {
+ $entry = '1.';
+ }
+ }
+ elsif ( $wagon->has_second_class ) {
+ $entry = '2.';
+ }
+ else {
+ $entry = $wagon->type;
+ }
+ }
+ if (
+ $group->train_no ne $departure->{train_no} )
+ {
+ $class = 'otherno';
+ }
+ push(
+ @{ $departure->{wr_preview} },
+ [ $entry, $class ]
+ );
+ $had_entry = 1;
+ }
+ }
+ if ($had_entry) {
+ $first = 1;
+ }
+ }
+ };
+ $departure->{wr_text} ||= 'Wagen';
return;
},
sub {
- $departure->{wr_link} = undef;
+ $departure->{wr_dt} = undef;
return;
}
)->finally(
@@ -786,10 +1002,13 @@ sub render_train {
}
if ($direction) {
- $departure->{direction} = $direction;
+ $departure->{wr_direction} = $direction;
+ $departure->{wr_direction_num} = $direction eq 'l' ? 0 : 100;
}
elsif ( $platform_info->{direction} ) {
- $departure->{direction} = 'a' . $platform_info->{direction};
+ $departure->{wr_direction} = 'a' . $platform_info->{direction};
+ $departure->{wr_direction_num}
+ = $platform_info->{direction} eq 'l' ? 0 : 100;
}
return;
@@ -805,95 +1024,58 @@ sub render_train {
}
)->wait;
- $self->hafas->get_route_timestamps_p( train => $result )->then(
- sub {
- my ( $route_ts, $journey ) = @_;
+ my %opt = ( train => $result );
- $departure->{trip_id} = $journey->id;
- $departure->{operator} = $journey->operator;
+ #if ( $self->languages =~ m{^en} ) {
+ # $opt{language} = 'en';
+ #}
- if ( my $load = $route_ts->{$station_name}{load} ) {
- if ( %{$load} ) {
- $departure->{utilization}
- = [ $load->{FIRST}, $load->{SECOND} ];
+ $self->hafas->get_route_p(%opt)->then(
+ sub {
+ my ( $route, $journey ) = @_;
+
+ $departure->{trip_id} = $journey->id;
+ $departure->{operators} = [ $journey->operators ];
+ $departure->{date} = $route->[0]{sched_dep} // $route->[0]{dep};
+
+ # Use HAFAS route as source of truth; ignore IRIS data
+ $departure->{route_pre_diff} = [];
+ $departure->{route_post_diff} = $route;
+ my $split;
+ for my $i ( 0 .. $#{ $departure->{route_post_diff} } ) {
+ if ( $departure->{route_post_diff}[$i]{name} eq $station_name )
+ {
+ $split = $i;
+ if ( my $load = $route->[$i]{load} ) {
+ if ( %{$load} ) {
+ $departure->{utilization}
+ = [ $load->{FIRST}, $load->{SECOND} ];
+ }
+ }
+ $departure->{tz_offset} = $route->[$i]{tz_offset};
+ $departure->{local_dt_da} = $route->[$i]{local_dt_da};
+ $departure->{local_sched_arr}
+ = $route->[$i]{local_sched_arr};
+ $departure->{local_sched_dep}
+ = $route->[$i]{local_sched_dep};
+ $departure->{is_annotated} = $route->[$i]{is_annotated};
+ $departure->{prod_name} = $route->[$i]{prod_name};
+ $departure->{direction} = $route->[$i]{direction};
+ $departure->{operator} = $route->[$i]{operator};
+ last;
}
}
- # If a train number changes on the way, IRIS routes are incomplete,
- # whereas HAFAS data has all stops -> merge HAFAS stops into IRIS
- # stops. This is a rare case, one point where it can be observed is
- # the TGV service at Frankfurt/Karlsruhe/Mannheim.
- my @hafas_stations = $journey->route;
- if ( my @iris_stations = @{ $departure->{route_pre_diff} } ) {
- my @missing_pre;
- for my $station (@hafas_stations) {
- if (
- List::MoreUtils::any { $_->{name} eq $station->{name} }
- @iris_stations
- )
- {
- unshift(
- @{ $departure->{route_pre_diff} },
- @missing_pre
- );
- last;
- }
+ if ( defined $split ) {
+ for my $i ( 0 .. $split - 1 ) {
push(
- @missing_pre,
- {
- name => $station->{name},
- hafas => 1
- }
+ @{ $departure->{route_pre_diff} },
+ shift( @{ $departure->{route_post_diff} } )
);
}
- }
- if ( my @iris_stations = @{ $departure->{route_post_diff} } ) {
- my @missing_post;
- for my $station ( reverse @hafas_stations ) {
- if (
- List::MoreUtils::any { $_->{name} eq $station->{name} }
- @iris_stations
- )
- {
- push(
- @{ $departure->{route_post_diff} },
- @missing_post
- );
- last;
- }
- unshift(
- @missing_post,
- {
- name => $station->{name},
- hafas => 1
- }
- );
- }
- }
-
- if ($route_ts) {
- if ( $route_ts->{ $result->station }{rt_bogus} ) {
- #$departure->{missing_realtime} = 1;
- }
- for my $elem (
- @{ $departure->{route_pre_diff} },
- @{ $departure->{route_post_diff} }
- )
- {
- if ( $elem->{name}
- =~ m{^Betriebsstelle nicht bekannt (\d+)$} )
- {
- my $eva = $1;
- if ( $route_ts->{$eva} ) {
- $elem->{name} = $route_ts->{$eva}{name};
- }
- }
- for my $key ( keys %{ $route_ts->{ $elem->{name} } // {} } )
- {
- $elem->{$key} = $route_ts->{ $elem->{name} }{$key};
- }
- }
+ # remove entry for $station_name
+ shift( @{ $departure->{route_post_diff} } );
}
my @him_messages;
@@ -936,63 +1118,45 @@ sub render_train {
}
)->wait;
- $departure->{composition}
- = $self->app->train_details_db->{ $departure->{train_no} };
- if ( not $departure->{arrival}
- and $departure->{composition}{prepTime}
- and $departure->{composition}{prepAt} eq $station_name )
- {
- $departure->{prep_time} = $departure->{composition}{prepTime};
- $departure->{arrival_hidden} = 1;
- }
- if ( $self->param('detailed') ) {
- my @cycle_from;
- my @cycle_to;
- for my $pred ( @{ $departure->{composition}{predecessors} // [] } ) {
- push( @cycle_from, $pred->[1] );
- }
- for my $succ ( @{ $departure->{composition}{successors} // [] } ) {
- push( @cycle_to, $succ->[1] );
- }
- $departure->{cycle_from}
- = [ map { [ $_, $self->app->train_details_db->{$_} ] } @cycle_from ];
- $departure->{cycle_to}
- = [ map { [ $_, $self->app->train_details_db->{$_} ] } @cycle_to ];
- }
-
# Defer rendering until all requests have completed
Mojo::Promise->all(@requests)->then(
sub {
- $self->render(
- $template // '_train_details',
- description => sprintf(
- '%s %s%s%s nach %s',
- $departure->{train_type},
- $departure->{train_line} // $departure->{train_no},
- $departure->{origin} ? ' von ' : q{},
- $departure->{origin} // q{},
- $departure->{destination} // 'unbekannt'
- ),
- departure => $departure,
- linetype => $linetype,
- icetype => $self->app->ice_type_map->{ $departure->{train_no} },
- details => $self->param('detailed')
- ? $departure->{composition} // {}
- : {},
- dt_now => DateTime->now( time_zone => 'Europe/Berlin' ),
- station_name => $station_name,
- nav_link =>
- $self->url_for( 'station', station => $station_name )->query(
- {
- detailed => $self->param('detailed'),
- hafas => $self->param('hafas')
- }
- ),
+ $self->respond_to(
+ json => {
+ json => {
+ departure => $departure,
+ station_name => $station_name,
+ },
+ },
+ any => {
+ template => $template // '_train_details',
+ description => sprintf(
+ '%s %s%s%s nach %s',
+ $departure->{train_type},
+ $departure->{train_line} // $departure->{train_no},
+ $departure->{origin} ? ' von ' : q{},
+ $departure->{origin} // q{},
+ $departure->{destination} // 'unbekannt'
+ ),
+ departure => $departure,
+ linetype => $linetype,
+ dt_now => DateTime->now( time_zone => 'Europe/Berlin' ),
+ station_name => $station_name,
+ nav_link =>
+ $self->url_for( 'station', station => $station_name )
+ ->query(
+ {
+ detailed => $self->param('detailed'),
+ hafas => $self->param('hafas')
+ }
+ ),
+ },
);
}
)->wait;
}
+# /z/:train/*station
sub station_train_details {
my ($self) = @_;
my $train_no = $self->stash('train');
@@ -1002,6 +1166,10 @@ sub station_train_details {
delete $self->stash->{layout};
}
+ if ( $station =~ s{ [.] json $ }{}x ) {
+ $self->stash( format => 'json' );
+ }
+
my %opt = (
cache_iris_main => $self->app->cache_iris_main,
cache_iris_rt => $self->app->cache_iris_rt,
@@ -1024,8 +1192,19 @@ sub station_train_details {
$opt{lookahead} = $self->config->{lookahead} + 20;
}
+ # Berlin Hbf exists twice:
+ # - BLS / 8011160
+ # - BL / 8098160 (formerly "Berlin Hbf (tief)")
+ # Right now DBF assumes that station name -> EVA / DS100 is a unique map.
+ # This is not the case. Work around it here until dbf has been adjusted
+ # properly.
+ if ( $station eq 'Berlin Hbf' ) {
+ $opt{with_related} = 1;
+ }
+
$self->render_later;
+ # Always performs an IRIS request
$self->get_results_p( $station, %opt )->then(
sub {
my ($status) = @_;
@@ -1066,6 +1245,8 @@ sub station_train_details {
arrival_is_cancelled => $result->arrival_is_cancelled,
moreinfo => $moreinfo,
delay => $result->delay,
+ arrival_delay => $result->arrival_delay,
+ departure_delay => $result->departure_delay,
route_pre => [ $result->route_pre ],
route_post => [ $result->route_post ],
replaced_by => [
@@ -1075,9 +1256,7 @@ sub station_train_details {
map { $_->type . q{ } . $_->train_no }
$result->replacement_for
],
- wr_link => $result->sched_departure
- ? $result->sched_departure->strftime('%Y%m%d%H%M')
- : undef,
+ wr_dt => $result->sched_departure,
eva => $result->station_uic,
start => $result->start,
};
@@ -1096,20 +1275,445 @@ sub station_train_details {
)->catch(
sub {
my ($errstr) = @_;
- $self->render(
- 'landingpage',
- error =>
- "Keine Abfahrt von $train_no in $station gefunden: $errstr",
- status => 404,
+ $self->respond_to(
+ json => {
+ json => {
+ error =>
+"Keine Abfahrt von $train_no in $station gefunden: $errstr",
+ },
+ status => 404,
+ },
+ any => {
+ template => 'landingpage',
+ error =>
+"Keine Abfahrt von $train_no in $station gefunden: $errstr",
+ status => 404,
+ },
);
return;
}
)->wait;
}
+sub train_details_dbris {
+ my ($self) = @_;
+ my $trip_id = $self->stash('train');
+
+ $self->render_later;
+
+ $self->dbris->get_journey_p( id => $trip_id )->then(
+ sub {
+ my ($dbris) = @_;
+ my $trip = $dbris->result;
+
+ my ( @him_messages, @him_details );
+ for my $message ( $trip->messages ) {
+ if ( not $message->{ueberschrift} ) {
+ push(
+ @him_messages,
+ [
+ q{},
+ {
+ icon => $message->{prioritaet} eq 'HOCH'
+ ? 'warning'
+ : 'info',
+ text => $message->{text}
+ }
+ ]
+ );
+ }
+ }
+
+ for my $attribute ( $trip->attributes ) {
+ push(
+ @him_details,
+ [
+ q{},
+ {
+ text => $attribute->{value}
+ . (
+ $attribute->{teilstreckenHinweis}
+ ? q { } . $attribute->{teilstreckenHinweis}
+ : q{}
+ )
+ }
+ ]
+ );
+ }
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $res = {
+ trip_id => $trip_id,
+ train_line => $trip->train,
+ train_no => $trip->number,
+ origin => ( $trip->route )[0]->name,
+ destination => ( $trip->route )[-1]->name,
+ operators => [],
+ linetype => 'bahn',
+ route_pre_diff => [],
+ route_post_diff => [],
+ moreinfo => [@him_messages],
+ details => [@him_details],
+ replaced_by => [],
+ replacement_for => [],
+ };
+
+ my $line = $trip->train;
+ if ( $line =~ m{ STR }x ) {
+ $res->{linetype} = 'tram';
+ }
+ elsif ( $line =~ m{ ^ S }x ) {
+ $res->{linetype} = 'sbahn';
+ }
+ elsif ( $line =~ m{ U }x ) {
+ $res->{linetype} = 'ubahn';
+ }
+ elsif ( $line =~ m{ Bus }x ) {
+ $res->{linetype} = 'bus';
+ }
+ elsif ( $line =~ m{ ^ [EI]CE? }x ) {
+ $res->{linetype} = 'fern';
+ }
+ elsif ( $line =~ m{ EST | FLX }x ) {
+ $res->{linetype} = 'ext';
+ }
+
+ my $station_is_past = 1;
+ for my $stop ( $trip->route ) {
+
+ push(
+ @{ $res->{route_post_diff} },
+ {
+ name => $stop->name,
+ eva => $stop->eva,
+ id => $stop->id,
+ sched_arr => $stop->sched_arr,
+ sched_dep => $stop->sched_dep,
+ rt_arr => $stop->rt_arr,
+ rt_dep => $stop->rt_dep,
+ arr_delay => $stop->arr_delay,
+ dep_delay => $stop->dep_delay,
+ platform => $stop->platform,
+ }
+ );
+ if (
+ $station_is_past
+ and $now->epoch < (
+ $res->{route_post_diff}[-1]{rt_arr}
+ // $res->{route_post_diff}[-1]{rt_dep}
+ // $res->{route_post_diff}[-1]{sched_arr}
+ // $res->{route_post_diff}[-1]{sched_dep} // $now
+ )->epoch
+ )
+ {
+ $station_is_past = 0;
+ }
+ $res->{route_post_diff}[-1]{isPast} = $station_is_past;
+ }
+
+ if ( my $req_id = $self->param('highlight') ) {
+ my $split;
+ for my $i ( 0 .. $#{ $res->{route_post_diff} } ) {
+ if ( $res->{route_post_diff}[$i]{eva} eq $req_id ) {
+ $split = $i;
+ last;
+ }
+ }
+ if ( defined $split ) {
+ $self->stash(
+ station_name => $res->{route_post_diff}[$split]{name} );
+ for my $i ( 0 .. $split - 1 ) {
+ push(
+ @{ $res->{route_pre_diff} },
+ shift( @{ $res->{route_post_diff} } )
+ );
+ }
+ my $station_info = shift( @{ $res->{route_post_diff} } );
+ $res->{eva} = $station_info->{eva};
+ if ( $station_info->{sched_arr} ) {
+ $res->{sched_arrival}
+ = $station_info->{sched_arr}->strftime('%H:%M');
+ }
+ if ( $station_info->{rt_arr} ) {
+ $res->{arrival}
+ = $station_info->{rt_arr}->strftime('%H:%M');
+ }
+ if ( $station_info->{sched_dep} ) {
+ $res->{sched_departure}
+ = $station_info->{sched_dep}->strftime('%H:%M');
+ }
+ if ( $station_info->{rt_dep} ) {
+ $res->{departure}
+ = $station_info->{rt_dep}->strftime('%H:%M');
+ }
+ $res->{arrival_is_cancelled}
+ = $station_info->{arr_cancelled};
+ $res->{departure_is_cancelled}
+ = $station_info->{dep_cancelled};
+ $res->{is_cancelled} = $res->{arrival_is_cancelled}
+ || $res->{arrival_is_cancelled};
+ $res->{tz_offset} = $station_info->{tz_offset};
+ $res->{local_dt_da} = $station_info->{local_dt_da};
+ $res->{local_sched_arr} = $station_info->{local_sched_arr};
+ $res->{local_sched_dep} = $station_info->{local_sched_dep};
+ $res->{is_annotated} = $station_info->{is_annotated};
+ $res->{prod_name} = $station_info->{prod_name};
+ $res->{direction} = $station_info->{direction};
+ $res->{operator} = $station_info->{operator};
+ $res->{platform} = $station_info->{platform};
+ $res->{scheduled_platform}
+ = $station_info->{sched_platform};
+ }
+ }
+
+ $self->respond_to(
+ json => {
+ json => {
+ journey => $trip,
+ },
+ },
+ any => {
+ template => $self->param('ajax')
+ ? '_train_details'
+ : 'train_details',
+ description => sprintf(
+ '%s %s%s%s nach %s',
+ $res->{train_type},
+ $res->{train_line} // $res->{train_no},
+ $res->{origin} ? ' von ' : q{},
+ $res->{origin} // q{},
+ $res->{destination} // 'unbekannt'
+ ),
+ departure => $res,
+ linetype => $res->{linetype},
+ dt_now => DateTime->now( time_zone => 'Europe/Berlin' ),
+ },
+ );
+ }
+ )->catch(
+ sub {
+ my ($e) = @_;
+ $self->respond_to(
+ json => {
+ json => {
+ error => $e,
+ },
+ status => 500,
+ },
+ any => {
+ template => 'exception',
+ message => $e,
+ exception => undef,
+ snapshot => {},
+ status => 500,
+ },
+ );
+ }
+ )->wait;
+}
+
+sub train_details_efa {
+ my ($self) = @_;
+ my $trip_id = $self->stash('train');
+
+ my $stopseq;
+ if ( $trip_id
+ =~ m{ ^ ([^@]*) @ ([^@]*) [(] ([^T]*) T ([^)]*) [)] (.*) $ }x )
+ {
+ $stopseq = {
+ stateless => $1,
+ stop_id => $2,
+ date => $3,
+ time => $4,
+ key => $5
+ };
+ }
+ else {
+ $self->render( 'not_found', status => 404 );
+ return;
+ }
+
+ $self->render_later;
+
+ Travel::Status::DE::EFA->new_p(
+ service => $self->param('efa'),
+ stopseq => $stopseq,
+ cache => $self->app->cache_iris_rt,
+ lwp_options => {
+ timeout => 10,
+ agent => 'dbf.finalrewind.org/2'
+ },
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ )->then(
+ sub {
+ my ($efa) = @_;
+ my $trip = $efa->result;
+
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ my $res = {
+ trip_id => $trip_id,
+ train_type => $trip->type,
+ train_line => $trip->line,
+ train_no => $trip->number,
+ origin => ( $trip->route )[0]->full_name,
+ destination => ( $trip->route )[-1]->full_name,
+ operators => [ $trip->operator ],
+ linetype => lc( $trip->product ) =~ tr{a-z}{}cdr,
+ route_pre_diff => [],
+ route_post_diff => [],
+ moreinfo => [],
+ replaced_by => [],
+ replacement_for => [],
+ };
+
+ if ( $res->{linetype} =~ m{strab|stra.?enbahn} ) {
+ $res->{linetype} = 'tram';
+ }
+ elsif ( $res->{linetype} =~ m{bus} ) {
+ $res->{linetype} = 'bus';
+ }
+
+ my $station_is_past = 1;
+ for my $stop ( $trip->route ) {
+
+ push(
+ @{ $res->{route_post_diff} },
+ {
+ name => $stop->full_name,
+ id => $stop->id_code,
+ sched_arr => $stop->sched_arr,
+ sched_dep => $stop->sched_dep,
+ rt_arr => $stop->rt_arr,
+ rt_dep => $stop->rt_dep,
+ arr_delay => $stop->arr_delay,
+ dep_delay => $stop->dep_delay,
+ platform => $stop->platform,
+ }
+ );
+ if (
+ $station_is_past
+ and $now->epoch < (
+ $res->{route_post_diff}[-1]{rt_arr}
+ // $res->{route_post_diff}[-1]{rt_dep}
+ // $res->{route_post_diff}[-1]{sched_arr}
+ // $res->{route_post_diff}[-1]{sched_dep} // $now
+ )->epoch
+ )
+ {
+ $station_is_past = 0;
+ }
+ $res->{route_post_diff}[-1]{isPast} = $station_is_past;
+ }
+
+ if ( my $req_id = $self->param('highlight') ) {
+ my $split;
+ for my $i ( 0 .. $#{ $res->{route_post_diff} } ) {
+ if ( $res->{route_post_diff}[$i]{id} eq $req_id ) {
+ $split = $i;
+ last;
+ }
+ }
+ if ( defined $split ) {
+ $self->stash(
+ station_name => $res->{route_post_diff}[$split]{name} );
+ for my $i ( 0 .. $split - 1 ) {
+ push(
+ @{ $res->{route_pre_diff} },
+ shift( @{ $res->{route_post_diff} } )
+ );
+ }
+ my $station_info = shift( @{ $res->{route_post_diff} } );
+ $res->{eva} = $station_info->{eva};
+ if ( $station_info->{sched_arr} ) {
+ $res->{sched_arrival}
+ = $station_info->{sched_arr}->strftime('%H:%M');
+ }
+ if ( $station_info->{rt_arr} ) {
+ $res->{arrival}
+ = $station_info->{rt_arr}->strftime('%H:%M');
+ }
+ if ( $station_info->{sched_dep} ) {
+ $res->{sched_departure}
+ = $station_info->{sched_dep}->strftime('%H:%M');
+ }
+ if ( $station_info->{rt_dep} ) {
+ $res->{departure}
+ = $station_info->{rt_dep}->strftime('%H:%M');
+ }
+ $res->{arrival_is_cancelled}
+ = $station_info->{arr_cancelled};
+ $res->{departure_is_cancelled}
+ = $station_info->{dep_cancelled};
+ $res->{is_cancelled} = $res->{arrival_is_cancelled}
+ || $res->{arrival_is_cancelled};
+ $res->{tz_offset} = $station_info->{tz_offset};
+ $res->{local_dt_da} = $station_info->{local_dt_da};
+ $res->{local_sched_arr} = $station_info->{local_sched_arr};
+ $res->{local_sched_dep} = $station_info->{local_sched_dep};
+ $res->{is_annotated} = $station_info->{is_annotated};
+ $res->{prod_name} = $station_info->{prod_name};
+ $res->{direction} = $station_info->{direction};
+ $res->{operator} = $station_info->{operator};
+ $res->{platform} = $station_info->{platform};
+ $res->{scheduled_platform}
+ = $station_info->{sched_platform};
+ }
+ }
+
+ $self->respond_to(
+ json => {
+ json => {
+ journey => $trip,
+ },
+ },
+ any => {
+ template => $self->param('ajax')
+ ? '_train_details'
+ : 'train_details',
+ description => sprintf(
+ '%s %s%s%s nach %s',
+ $res->{train_type},
+ $res->{train_line} // $res->{train_no},
+ $res->{origin} ? ' von ' : q{},
+ $res->{origin} // q{},
+ $res->{destination} // 'unbekannt'
+ ),
+ departure => $res,
+ linetype => $res->{linetype},
+ dt_now => DateTime->now( time_zone => 'Europe/Berlin' ),
+ },
+ );
+ }
+ )->catch(
+ sub {
+ my ($e) = @_;
+ $self->respond_to(
+ json => {
+ json => {
+ error => $e,
+ },
+ status => 500,
+ },
+ any => {
+ template => 'exception',
+ message => $e,
+ exception => undef,
+ snapshot => {},
+ status => 500,
+ },
+ );
+ }
+ )->wait;
+}
+
+# /z/:train
sub train_details {
my ($self) = @_;
- my $train = $self->stash('train');
+ my $train = $self->stash('train');
+ my $dbris = $self->param('dbris');
+ my $efa = $self->param('efa');
+ my $hafas = $self->param('hafas');
# TODO error handling
@@ -1117,11 +1721,16 @@ sub train_details {
delete $self->stash->{layout};
}
- my $api_version = $Travel::Status::DE::IRIS::VERSION;
-
$self->stash( departures => [] );
$self->stash( title => 'DBF' );
+ if ($dbris) {
+ return $self->train_details_dbris;
+ }
+ if ($efa) {
+ return $self->train_details_efa;
+ }
+
my $res = {
train_type => undef,
train_line => undef,
@@ -1147,63 +1756,96 @@ sub train_details {
$opt{train_no} = $train_no;
}
+ my $service = 'DB';
+ if ( $hafas
+ and $hafas ne '1'
+ and Travel::Status::DE::HAFAS::get_service($hafas) )
+ {
+ $opt{service} = $hafas;
+ }
+
+ #if ( $self->languages =~ m{^en} ) {
+ # $opt{language} = 'en';
+ #}
+
+ if ( my $date = $self->param('date') ) {
+ if ( $date
+ =~ m{ ^ (?<day> \d{1,2} ) [.] (?<month> \d{1,2} ) [.] (?<year> \d{4})? $ }x
+ )
+ {
+ $opt{datetime} = DateTime->now( time_zone => 'Europe/Berlin' );
+ $opt{datetime}->set(
+ day => $+{day},
+ month => $+{month}
+ );
+ if ( $+{year} ) {
+ $opt{datetime}->set( year => $+{year} );
+ }
+ }
+ }
+
$self->stash( hide_opts => 1 );
$self->render_later;
my $linetype = 'bahn';
- $self->hafas->get_route_timestamps_p(%opt)->then(
+ $self->hafas->get_route_p(%opt)->then(
sub {
- my ( $route_ts, $journey ) = @_;
+ my ( $route, $journey, $hafas_obj ) = @_;
$res->{trip_id} = $journey->id;
+ $res->{date} = $route->[0]{sched_dep} // $route->[0]{dep};
+
+ my $product = $journey->product;
- if ( not $res->{train_type} ) {
- my $train_type = $res->{train_type} = $journey->type // q{};
- my $train_no = $res->{train_no} = $journey->number // q{};
- $res->{train_line} = $journey->line_no // q{};
- $self->stash( title => "${train_type} ${train_no}" );
+ if ( my $req_name = $self->param('highlight') ) {
+ if ( my $p = $journey->product_at($req_name) ) {
+ $product = $p;
+ }
}
- if ( not defined $journey->class ) {
+ my $train_type = $res->{train_type} = $product->type // q{};
+ my $train_no = $res->{train_no} = $product->number // q{};
+ $res->{train_line} = $product->line_no // q{};
+ $self->stash( title => $train_type . ' '
+ . ( $train_no || $res->{train_line} ) );
+
+ if ( not defined $product->class ) {
$linetype = 'ext';
}
- elsif ( $journey->class <= 2 ) {
- $linetype = 'fern';
- }
- elsif ( $journey->class <= 8 ) {
- $linetype = 'bahn';
- }
- elsif ( $journey->class <= 16 ) {
- $linetype = 'sbahn';
- }
- elsif ( $journey->class == 32 ) {
- $linetype = 'bus';
- }
- elsif ( $journey->class == 128 ) {
- $linetype = 'ubahn';
- }
- elsif ( $journey->class == 256 ) {
- $linetype = 'tram';
+ else {
+ my $prod
+ = $self->class_to_product($hafas_obj)->{ $product->class }
+ // q{};
+ if ( $prod =~ m{ ^ ice? | inter-?cit }ix ) {
+ $linetype = 'fern';
+ }
+ elsif ( $prod =~ m{ s-bahn | urban | rapid }ix ) {
+ $linetype = 'sbahn';
+ }
+ elsif ( $prod =~ m{ bus }ix ) {
+ $linetype = 'bus';
+ }
+ elsif ( $prod =~ m{ metro | u-bahn | subway }ix ) {
+ $linetype = 'ubahn';
+ }
+ elsif ( $prod =~ m{ tram }ix ) {
+ $linetype = 'tram';
+ }
}
$res->{origin} = $journey->route_start;
$res->{destination} = $journey->route_end;
- $res->{operator} = $journey->operator;
+ $res->{operators} = [ $journey->operators ];
- $res->{route_post_diff}
- = [ map { { name => $_->{name} } } $journey->route ];
- for my $elem ( @{ $res->{route_post_diff} } ) {
- for my $key ( keys %{ $route_ts->{ $elem->{name} } // {} } ) {
- $elem->{$key} = $route_ts->{ $elem->{name} }{$key};
- }
- }
+ $res->{route_post_diff} = $route;
if ( my $req_name = $self->param('highlight') ) {
my $split;
for my $i ( 0 .. $#{ $res->{route_post_diff} } ) {
if ( $res->{route_post_diff}[$i]{name} eq $req_name ) {
$split = $i;
+ last;
}
}
if ( defined $split ) {
@@ -1238,7 +1880,15 @@ sub train_details {
= $station_info->{dep_cancelled};
$res->{is_cancelled} = $res->{arrival_is_cancelled}
|| $res->{arrival_is_cancelled};
- $res->{platform} = $station_info->{platform};
+ $res->{tz_offset} = $station_info->{tz_offset};
+ $res->{local_dt_da} = $station_info->{local_dt_da};
+ $res->{local_sched_arr} = $station_info->{local_sched_arr};
+ $res->{local_sched_dep} = $station_info->{local_sched_dep};
+ $res->{is_annotated} = $station_info->{is_annotated};
+ $res->{prod_name} = $station_info->{prod_name};
+ $res->{direction} = $station_info->{direction};
+ $res->{operator} = $station_info->{operator};
+ $res->{platform} = $station_info->{platform};
$res->{scheduled_platform}
= $station_info->{sched_platform};
}
@@ -1276,61 +1926,308 @@ sub train_details {
$res->{details} = [@him_details];
}
- if ( $self->param('detailed') ) {
- $res->{composition}
- = $self->app->train_details_db->{ $res->{train_no} };
- my @cycle_from;
- my @cycle_to;
- for my $pred ( @{ $res->{composition}{predecessors} // [] } ) {
- push( @cycle_from, $pred->[1] );
- }
- for my $succ ( @{ $res->{composition}{successors} // [] } ) {
- push( @cycle_to, $succ->[1] );
- }
- $res->{cycle_from}
- = [ map { [ $_, $self->app->train_details_db->{$_} ] }
- @cycle_from ];
- $res->{cycle_to}
- = [ map { [ $_, $self->app->train_details_db->{$_} ] }
- @cycle_to ];
- }
-
- $self->render(
- $self->param('ajax') ? '_train_details' : 'train_details',
- description => sprintf(
- '%s %s%s%s nach %s',
- $res->{train_type},
- $res->{train_line} // $res->{train_no},
- $res->{origin} ? ' von ' : q{},
- $res->{origin} // q{},
- $res->{destination} // 'unbekannt'
- ),
- departure => $res,
- linetype => $linetype,
- icetype => $self->app->ice_type_map->{ $res->{train_no} },
- details => {}, #$departure->{composition} // {},
- dt_now => DateTime->now( time_zone => 'Europe/Berlin' ),
+ $self->respond_to(
+ json => {
+ json => {
+ journey => $journey,
+ },
+ },
+ any => {
+ template => $self->param('ajax')
+ ? '_train_details'
+ : 'train_details',
+ description => sprintf(
+ '%s %s%s%s nach %s',
+ $res->{train_type},
+ $res->{train_line} // $res->{train_no},
+ $res->{origin} ? ' von ' : q{},
+ $res->{origin} // q{},
+ $res->{destination} // 'unbekannt'
+ ),
+ departure => $res,
+ linetype => $linetype,
+ dt_now => DateTime->now( time_zone => 'Europe/Berlin' ),
+ },
);
}
)->catch(
sub {
my ($e) = @_;
if ($e) {
- $self->render(
- 'exception',
- message => $e,
- exception => undef,
- snapshot => {}
+ $self->respond_to(
+ json => {
+ json => {
+ error => $e,
+ },
+ status => 500,
+ },
+ any => {
+ template => 'exception',
+ message => $e,
+ exception => undef,
+ snapshot => {},
+ status => 500,
+ },
);
}
else {
- $self->render('not_found');
+ $self->render( 'not_found', status => 404 );
}
}
)->wait;
}
-sub handle_result {
+sub render_board_dbris {
+ my ( $self, $station_id, $dbris ) = @_;
+ my $template = $self->param('mode') // 'app';
+ my $hide_low_delay = $self->param('hidelowdelay') // 0;
+ my $hide_opts = $self->param('hide_opts') // 0;
+ my $show_realtime = $self->param('rt') // $self->param('show_realtime')
+ // 1;
+
+ my $station_name;
+ if ( $station_id =~ m{ [@] O = (?<name> [^@]+) [@] }x ) {
+ $station_name = $+{name};
+ }
+
+ my @departures;
+
+ if ( $self->param('ajax') ) {
+ delete $self->stash->{layout};
+ }
+
+ my @results = $self->filter_results( $dbris->results );
+
+ @results = map { $_->[1] } sort { $a->[0] <=> $b->[0] }
+ map { [ $_->dep, $_ ] } @results;
+
+ for my $result (@results) {
+ my $time;
+
+ if ( $template eq 'json' ) {
+ push( @departures, $result );
+ next;
+ }
+
+ if ( $show_realtime and $result->rt_dep ) {
+ $time = $result->rt_dep->strftime('%H:%M');
+ }
+ else {
+ $time = $result->sched_dep->strftime('%H:%M');
+ }
+
+ my $linetype = $result->line;
+ if ( $linetype =~ m{ STR }x ) {
+ $linetype = 'tram';
+ }
+ elsif ( $linetype =~ m{ ^ S }x ) {
+ $linetype = 'sbahn';
+ }
+ elsif ( $linetype =~ m{ U }x ) {
+ $linetype = 'ubahn';
+ }
+ elsif ( $linetype =~ m{ Bus }x ) {
+ $linetype = 'bus';
+ }
+ elsif ( $linetype =~ m{ ^ [EI]CE? }x ) {
+ $linetype = 'fern';
+ }
+ elsif ( $linetype =~ m{ EST | FLX }x ) {
+ $linetype = 'ext';
+ }
+ else {
+ $linetype = 'bahn';
+ }
+
+ my $delay = $result->delay;
+
+ push(
+ @departures,
+ {
+ time => $time,
+ sched_departure => $result->sched_dep->strftime('%H:%M'),
+ departure => $result->rt_dep
+ ? $result->rt_dep->strftime('%H:%M')
+ : undef,
+ train => $result->train_mid,
+ train_type => q{},
+ train_line => $result->line,
+ train_no => $result->maybe_train_no,
+ journey_id => $result->id,
+ via => [ $result->via ],
+ origin => q{},
+ destination => $result->destination,
+ platform => $result->rt_platform // $result->platform,
+ scheduled_platform => $result->platform,
+ is_cancelled => $result->is_cancelled,
+ linetype => $linetype,
+ delay => $delay,
+ is_bit_delayed =>
+ ( $delay and $delay > 0 and $delay < 5 ? 1 : 0 ),
+ is_delayed => ( $delay and $delay >= 5 ? 1 : 0 ),
+ has_realtime => defined $delay ? 1 : 0,
+ station => $result->stop_eva,
+ replaced_by => [],
+ replacement_for => [],
+ route_pre => [],
+ route_post => [ $result->via ],
+ wr_dt => undef,
+ }
+ );
+ }
+
+ if ( $template eq 'json' ) {
+ $self->res->headers->access_control_allow_origin(q{*});
+ my $json = {
+ departures => \@departures,
+ };
+ $self->render(
+ json => $json,
+ );
+ }
+ else {
+ $self->render(
+ $template,
+ description => "Abfahrtstafel $station_name",
+ departures => \@departures,
+ station => $station_name,
+ version => $self->config->{version},
+ title => $station_name,
+ refresh_interval => $template eq 'app' ? 0 : 120,
+ hide_opts => $hide_opts,
+ hide_footer => $hide_opts,
+ hide_low_delay => $hide_low_delay,
+ show_realtime => $show_realtime,
+ load_marquee => (
+ $template eq 'single'
+ or $template eq 'multi'
+ ),
+ force_mobile => ( $template eq 'app' ),
+ );
+ }
+}
+
+sub render_board_efa {
+ my ( $self, $station_name, $efa ) = @_;
+ my $template = $self->param('mode') // 'app';
+ my $hide_low_delay = $self->param('hidelowdelay') // 0;
+ my $hide_opts = $self->param('hide_opts') // 0;
+ my $show_realtime = $self->param('rt') // $self->param('show_realtime')
+ // 1;
+
+ my @departures;
+
+ if ( $self->param('ajax') ) {
+ delete $self->stash->{layout};
+ }
+
+ my @results = $self->filter_results( $efa->results );
+
+ for my $result (@results) {
+ my $time;
+
+ if ( $template eq 'json' ) {
+ push( @departures, $result );
+ next;
+ }
+
+ if ( $show_realtime and $result->rt_datetime ) {
+ $time = $result->rt_datetime->strftime('%H:%M');
+ }
+ else {
+ $time = $result->sched_datetime->strftime('%H:%M');
+ }
+
+ my $linetype = $result->mot_name // 'bahn';
+ if ( $linetype =~ m{ s-bahn | urban | rapid }ix ) {
+ $linetype = 'sbahn';
+ }
+ elsif ( $linetype =~ m{ metro | u-bahn | subway }ix ) {
+ $linetype = 'ubahn';
+ }
+ elsif ( $linetype =~ m{ bus }ix ) {
+ $linetype = 'bus';
+ }
+ elsif ( $linetype =~ m{ tram }ix ) {
+ $linetype = 'tram';
+ }
+ elsif ( $linetype =~ m{ ^ ice? | inter-?cit }ix ) {
+ $linetype = 'fern';
+ }
+ elsif ( $linetype eq 'sonstige' ) {
+ $linetype = 'ext';
+ }
+
+ my $delay = $result->delay;
+
+ push(
+ @departures,
+ {
+ time => $time,
+ sched_departure => $result->sched_datetime->strftime('%H:%M'),
+ departure => $result->rt_datetime
+ ? $result->rt_datetime->strftime('%H:%M')
+ : undef,
+ train => $result->line,
+ train_type => q{},
+ train_line => $result->line,
+ train_no => $result->train_no,
+ journey_id => $result->id,
+ via => [ map { $_->name } $result->route_interesting ],
+ origin => $result->origin,
+ destination => $result->destination,
+ platform => $result->platform,
+ is_cancelled => $result->is_cancelled,
+ linetype => $linetype,
+ delay => $delay,
+ is_bit_delayed =>
+ ( $delay and $delay > 0 and $delay < 5 ? 1 : 0 ),
+ is_delayed => ( $delay and $delay >= 5 ? 1 : 0 ),
+ has_realtime => defined $delay ? 1 : 0,
+ occupancy => $result->occupancy,
+ station => $efa->stop->id_code,
+ replaced_by => [],
+ replacement_for => [],
+ route_pre => [ map { $_->full_name } $result->route_pre ],
+ route_post => [ map { $_->full_name } $result->route_post ],
+ wr_dt => undef,
+ }
+ );
+ }
+
+ if ( $template eq 'json' ) {
+ $self->res->headers->access_control_allow_origin(q{*});
+ my $json = {
+ departures => \@departures,
+ };
+ $self->render(
+ json => $json,
+ );
+ }
+ else {
+ $self->render(
+ $template,
+ description => "Abfahrtstafel $station_name",
+ departures => \@departures,
+ station => $efa->stop->name,
+ version => $self->config->{version},
+ title => $efa->stop->name // $station_name,
+ refresh_interval => $template eq 'app' ? 0 : 120,
+ hide_opts => $hide_opts,
+ hide_footer => $hide_opts,
+ hide_low_delay => $hide_low_delay,
+ show_realtime => $show_realtime,
+ load_marquee => (
+ $template eq 'single'
+ or $template eq 'multi'
+ ),
+ force_mobile => ( $template eq 'app' ),
+ );
+ }
+}
+
+# For HAFAS and IRIS departure elements
+sub render_board_hafas {
my ( $self, $data ) = @_;
my @results = @{ $data->{results} };
@@ -1341,13 +2238,14 @@ sub handle_result {
my $hide_low_delay = $self->param('hidelowdelay') // 0;
my $hide_opts = $self->param('hide_opts') // 0;
my $show_realtime = $self->param('rt') // $self->param('show_realtime')
- // 0;
+ // 1;
my $show_details = $self->param('detailed') // 0;
my $admode = $self->param('admode') // 'deparr';
my $apiver = $self->param('version') // 0;
my $callback = $self->param('callback');
my $via = $self->param('via');
my $hafas = $self->param('hafas');
+ my $hafas_obj = $data->{hafas};
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
@@ -1382,19 +2280,38 @@ sub handle_result {
@results = sort { $a->datetime <=> $b->datetime } @results;
}
elsif ( $admode eq 'arr' ) {
- @results = sort {
- ( $a->arrival // $a->departure )
- <=> ( $b->arrival // $b->departure )
- } @results;
+ @results = map { $_->[1] }
+ sort { $a->[0] <=> $b->[0] }
+ map {
+ [
+ (
+ $_->sched_arrival ? $_->arrival_is_cancelled
+ : $_->is_cancelled
+ ) ? ( $_->sched_arrival // $_->sched_departure )
+ : ( $_->arrival // $_->departure ),
+ $_
+ ]
+ } @results;
}
else {
- @results = sort {
- ( $a->departure // $a->arrival )
- <=> ( $b->departure // $b->arrival )
- } @results;
+ @results = map { $_->[1] }
+ sort { $a->[0] <=> $b->[0] }
+ map {
+ [
+ (
+ $_->sched_departure ? $_->departure_is_cancelled
+ : $_->is_cancelled
+ ) ? ( $_->sched_departure // $_->sched_arrival )
+ : ( $_->departure // $_->arrival ),
+ $_
+ ]
+ } @results;
}
}
+ my $class_to_product
+ = $hafas_obj ? $self->class_to_product($hafas_obj) : {};
+
@results = $self->filter_results(@results);
for my $result (@results) {
@@ -1434,19 +2351,20 @@ sub handle_result {
}
}
elsif ( $result->can('class') ) {
- if ( $result->class <= 2 ) {
+ my $prod = $class_to_product->{ $result->class } // q{};
+ if ( $prod =~ m{ ^ ice? | inter-?cit }ix ) {
$linetype = 'fern';
}
- elsif ( $result->class == 16 ) {
+ elsif ( $prod =~ m{ s-bahn | urban | rapid }ix ) {
$linetype = 'sbahn';
}
- elsif ( $result->class == 32 ) {
+ elsif ( $prod =~ m{ bus }ix ) {
$linetype = 'bus';
}
- elsif ( $result->class == 128 ) {
+ elsif ( $prod =~ m{ metro | u-bahn | subway }ix ) {
$linetype = 'ubahn';
}
- elsif ( $result->class == 256 ) {
+ elsif ( $prod =~ m{ tram }ix ) {
$linetype = 'tram';
}
}
@@ -1481,8 +2399,14 @@ sub handle_result {
}
if ( $template eq 'json' ) {
- my @json_route = $self->json_route_diff( [ $result->route ],
- [ $result->sched_route ] );
+ my @json_route;
+ if ( $result->can('sched_route') ) {
+ @json_route = $self->json_route_diff( [ $result->route ],
+ [ $result->sched_route ] );
+ }
+ else {
+ @json_route = map { $_->TO_JSON } $result->route;
+ }
if ( $apiver eq '1' or $apiver eq '2' ) {
@@ -1497,64 +2421,90 @@ sub handle_result {
);
return;
}
+ elsif ( $apiver eq 'raw' ) {
+ push( @departures, $result );
+ }
else { # apiver == 3
- my ( $delay_arr, $delay_dep, $sched_arr, $sched_dep );
- if ( $result->arrival ) {
- $delay_arr = $result->arrival->subtract_datetime(
- $result->sched_arrival )->in_units('minutes');
- }
- if ( $result->departure ) {
- $delay_dep = $result->departure->subtract_datetime(
- $result->sched_departure )->in_units('minutes');
- }
- if ( $result->sched_arrival ) {
- $sched_arr = $result->sched_arrival->strftime('%H:%M');
+ if ( $result->isa('Travel::Status::DE::IRIS::Result') ) {
+ my ( $delay_arr, $delay_dep, $sched_arr, $sched_dep );
+ if ( $result->arrival ) {
+ $delay_arr = $result->arrival->subtract_datetime(
+ $result->sched_arrival )->in_units('minutes');
+ }
+ if ( $result->departure ) {
+ $delay_dep = $result->departure->subtract_datetime(
+ $result->sched_departure )->in_units('minutes');
+ }
+ if ( $result->sched_arrival ) {
+ $sched_arr = $result->sched_arrival->strftime('%H:%M');
+ }
+ if ( $result->sched_departure ) {
+ $sched_dep
+ = $result->sched_departure->strftime('%H:%M');
+ }
+ push(
+ @departures,
+ {
+ delayArrival => $delay_arr,
+ delayDeparture => $delay_dep,
+ destination => $result->destination,
+ isCancelled => $result->is_cancelled,
+ messages => {
+ delay => [
+ map {
+ {
+ timestamp => $_->[0],
+ text => $_->[1]
+ }
+ } $result->delay_messages
+ ],
+ qos => [
+ map {
+ {
+ timestamp => $_->[0],
+ text => $_->[1]
+ }
+ } $result->qos_messages
+ ],
+ },
+ missingRealtime => (
+ (
+ not $result->has_realtime
+ and $result->start < $now
+ ) ? \1 : \0
+ ),
+ platform => $result->platform,
+ route => \@json_route,
+ scheduledPlatform => $result->sched_platform,
+ scheduledArrival => $sched_arr,
+ scheduledDeparture => $sched_dep,
+ train => $result->train,
+ trainClasses => [ $result->classes ],
+ trainNumber => $result->train_no,
+ via => [ $result->route_interesting(3) ],
+ }
+ );
}
- if ( $result->sched_departure ) {
- $sched_dep = $result->sched_departure->strftime('%H:%M');
+ else {
+ push(
+ @departures,
+ {
+ delay => $result->delay,
+ direction => $result->direction,
+ destination => $result->destination,
+ isCancelled => $result->is_cancelled,
+ messages => [ $result->messages ],
+ platform => $result->platform,
+ route => \@json_route,
+ scheduledPlatform => $result->sched_platform,
+ scheduledTime => $result->sched_datetime->epoch,
+ time => $result->datetime->epoch,
+ train => $result->line,
+ trainNumber => $result->number,
+ via => [ $result->route_interesting(3) ],
+ }
+ );
}
- push(
- @departures,
- {
- delayArrival => $delay_arr,
- delayDeparture => $delay_dep,
- destination => $result->destination,
- isCancelled => $result->is_cancelled,
- messages => {
- delay => [
- map {
- {
- timestamp => $_->[0],
- text => $_->[1]
- }
- } $result->delay_messages
- ],
- qos => [
- map {
- {
- timestamp => $_->[0],
- text => $_->[1]
- }
- } $result->qos_messages
- ],
- },
- missingRealtime => (
- (
- not $result->has_realtime
- and $result->start < $now
- ) ? \1 : \0
- ),
- platform => $result->platform,
- route => \@json_route,
- scheduledPlatform => $result->sched_platform,
- scheduledArrival => $sched_arr,
- scheduledDeparture => $sched_dep,
- train => $result->train,
- trainClasses => [ $result->classes ],
- trainNumber => $result->train_no,
- via => [ $result->route_interesting(3) ],
- }
- );
}
}
elsif ( $template eq 'text' ) {
@@ -1622,9 +2572,15 @@ sub handle_result {
} $result->qos_messages
],
},
- station => $result->station,
- moreinfo => $moreinfo,
- delay => $delay,
+ station => $result->station,
+ moreinfo => $moreinfo,
+ delay => $delay,
+ is_bit_delayed =>
+ ( $delay and $delay > 0 and $delay < 5 ? 1 : 0 ),
+ is_delayed => ( $delay and $delay >= 5 ? 1 : 0 ),
+ arrival_delay => $result->arrival_delay,
+ departure_delay => $result->departure_delay,
+ has_realtime => $result->has_realtime,
missing_realtime => (
not $result->has_realtime
and $result->start < $now ? 1 : 0
@@ -1641,9 +2597,8 @@ sub handle_result {
map { $_->type . q{ } . $_->train_no }
$result->replacement_for
],
- wr_link => $result->sched_departure
- ? $result->sched_departure->strftime('%Y%m%d%H%M')
- : undef,
+ wr_dt => $result->sched_departure,
+ eva => $result->station_uic,
}
);
}
@@ -1670,29 +2625,33 @@ sub handle_result {
train_no => $result->number,
journey_id => $result->id,
via => [
- map { $_->{name} =~ s{,\Q$city\E}{}r }
+ map { $_->loc->name =~ s{,\Q$city\E}{}r }
$result->route_interesting(3)
],
destination => $result->route_end =~ s{,\Q$city\E}{}r,
origin => $result->route_end =~ s{,\Q$city\E}{}r,
platform => $result->platform,
scheduled_platform => $result->sched_platform,
+ load => $result->load // {},
info => $info,
is_cancelled => $result->is_cancelled,
linetype => $linetype,
station => $result->station,
moreinfo => $moreinfo,
delay => $delay,
- replaced_by => [],
- replacement_for => [],
- route_pre => $admode eq 'arr'
- ? [ map { $_->{name} } $result->route ]
+ is_bit_delayed =>
+ ( $delay and $delay > 0 and $delay < 5 ? 1 : 0 ),
+ is_delayed => ( $delay and $delay >= 5 ? 1 : 0 ),
+ has_realtime => defined $delay ? 1 : 0,
+ replaced_by => [],
+ replacement_for => [],
+ route_pre => $admode eq 'arr'
+ ? [ map { $_->loc->name } $result->route ]
: [],
route_post => $admode eq 'arr' ? []
- : [ map { $_->{name} } $result->route ],
- wr_link => $result->sched_datetime
- ? $result->sched_datetime->strftime('%Y%m%d%H%M')
- : undef,
+ : [ map { $_->loc->name } $result->route ],
+ wr_dt => $result->sched_datetime,
+ eva => $result->station_uic,
}
);
}
@@ -1743,19 +2702,22 @@ sub handle_result {
my $station_name = $data->{station_name} // $self->stash('station');
my ( $api_link, $api_text, $api_icon );
my $params = $self->req->params->clone;
- $params->param( hafas => not $params->param('hafas') );
- if ( $params->param('hafas') ) {
- $api_link = '/' . $data->{station_eva} . '?' . $params->to_string;
- $api_text = 'Auf Nahverkehr wechseln';
- $api_icon = 'train';
- }
- else {
- my $iris_eva = List::Util::min grep { $_ >= 1000000 }
- @{ $data->{station_evas} // [] };
- if ($iris_eva) {
- $api_link = '/' . $iris_eva . '?' . $params->to_string;
- $api_text = 'Auf Bahnverkehr wechseln';
- $api_icon = 'directions';
+ if ( not $hafas ) {
+ if ( $data->{station_eva} >= 8100000
+ and $data->{station_eva} < 8200000 )
+ {
+ $params->param( hafas => 'ÖBB' );
+ }
+ elsif ( $data->{station_eva} >= 8500000
+ and $data->{station_eva} < 8600000 )
+ {
+ $params->param( hafas => 'BLS' );
+ }
+ if ( $params->param('hafas') ) {
+ $api_link
+ = '/' . $data->{station_eva} . '?' . $params->to_string;
+ $api_text = 'Auf Nahverkehr wechseln';
+ $api_icon = 'train';
}
}
$self->render(
@@ -1766,12 +2728,12 @@ sub handle_result {
api_text => $api_text,
api_icon => $api_icon,
departures => \@departures,
- ice_type => $self->app->ice_type_map,
station => $station_name,
version => $self->config->{version},
title => $via ? "$station_name → $via" : $station_name,
refresh_interval => $template eq 'app' ? 0 : 120,
hide_opts => $hide_opts,
+ hide_footer => $hide_opts,
hide_low_delay => $hide_low_delay,
show_realtime => $show_realtime,
load_marquee => (
@@ -1794,16 +2756,66 @@ sub handle_result {
sub stations_by_coordinates {
my $self = shift;
- my $lon = $self->param('lon');
- my $lat = $self->param('lat');
+ my $lon = $self->param('lon');
+ my $lat = $self->param('lat');
+ my $efa_service = $self->param('efa');
+ my $hafas = $self->param('hafas');
if ( not $lon or not $lat ) {
$self->render( json => { error => 'Invalid lon/lat received' } );
return;
}
+ my $service = 'ÖBB';
+ if ( $hafas
+ and $hafas ne '1'
+ and Travel::Status::DE::HAFAS::get_service($hafas) )
+ {
+ $service = $hafas;
+ }
+
$self->render_later;
+ if ($efa_service) {
+ Travel::Status::DE::EFA->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => $self->ua,
+ service => $efa_service,
+ coord => {
+ lat => $lat,
+ lon => $lon
+ }
+ )->then(
+ sub {
+ my ($efa) = @_;
+ my @efa = map {
+ {
+ name => $_->full_name,
+ eva => $_->id =~ s{:}{%3A}gr,
+ distance => $_->distance_m / 1000,
+ efa => $efa_service,
+ }
+ } $efa->results;
+ $self->render(
+ json => {
+ candidates => [@efa],
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ warning => $err,
+ }
+ );
+ }
+ )->wait;
+ return;
+ }
+
my @iris = map {
{
ds100 => $_->[0][0],
@@ -1821,7 +2833,8 @@ sub stations_by_coordinates {
Travel::Status::DE::HAFAS->new_p(
promise => 'Mojo::Promise',
- user_agent => $self->ua,
+ user_agent => $service eq 'PKP' ? Mojo::UserAgent->new : $self->ua,
+ service => $service,
geoSearch => {
lat => $lat,
lon => $lon
@@ -1834,7 +2847,7 @@ sub stations_by_coordinates {
name => $_->name,
eva => $_->eva,
distance => $_->distance_m / 1000,
- hafas => 1
+ hafas => $service,
}
} $hafas->results;
if ( @hafas > 10 ) {
@@ -1862,6 +2875,101 @@ sub stations_by_coordinates {
)->wait;
}
+sub backend_list {
+ my ($self) = @_;
+
+ my %place_map = (
+ AT => 'Österreich',
+ CH => 'Schweiz',
+ 'CH-BE' => 'Kanton Bern',
+ 'CH-GE' => 'Kanton Genf',
+ 'CH-LU' => 'Kanton Luzern',
+ 'CH-ZH' => 'Kanton Zürich',
+ DE => 'Deutschland',
+ 'DE-BB' => 'Brandenburg',
+ 'DE-BW' => 'Baden-Württemberg',
+ 'DE-BE' => 'Berlin',
+ 'DE-BY' => 'Bayern',
+ 'DE-HB' => 'Bremen',
+ 'DE-HE' => 'Hessen',
+ 'DE-MV' => 'Mecklenburg-Vorpommern',
+ 'DE-NI' => 'Niedersachsen',
+ 'DE-NW' => 'Nordrhein-Westfalen',
+ 'DE-RP' => 'Rheinland-Pfalz',
+ 'DE-SH' => 'Schleswig-Holstein',
+ 'DE-ST' => 'Sachsen-Anhalt',
+ 'DE-TH' => 'Thüringen',
+ DK => 'Dänemark',
+ 'GB-NIR' => 'Nordirland',
+ LI => 'Liechtenstein',
+ LU => 'Luxembourg',
+ IE => 'Irland',
+ 'US-CA' => 'California',
+ 'US-TX' => 'Texas',
+ );
+
+ my @backends = (
+ {
+ name => 'Deutsche Bahn',
+ type => 'IRIS-TTS',
+ }
+ );
+
+ for my $backend ( Travel::Status::DE::EFA::get_services() ) {
+ push(
+ @backends,
+ {
+ name => $backend->{name},
+ shortname => $backend->{shortname},
+ homepage => $backend->{homepage},
+ regions => [
+ map { $place_map{$_} // $_ }
+ @{ $backend->{coverage}{regions} }
+ ],
+ has_area => $backend->{coverage}{area} ? 1 : 0,
+ type => 'EFA',
+ efa => 1,
+ }
+ );
+ }
+
+ for my $backend ( Travel::Status::DE::HAFAS::get_services() ) {
+ if ( $backend->{shortname} eq 'DB' ) {
+
+ # HTTP 503 Service Temporarily Unavailable as of 2025-01-08 ~10:30 UTC
+ # (I bet it's actually Permanently Unavailable)
+ next;
+ }
+ if ( $backend->{shortname} eq 'VRN' ) {
+
+ # HTTP 403 Forbidden as of 2025-03-03
+ next;
+ }
+ push(
+ @backends,
+ {
+ name => $backend->{name},
+ shortname => $backend->{shortname},
+ homepage => $backend->{homepage},
+ regions => [
+ map { $place_map{$_} // $_ }
+ @{ $backend->{coverage}{regions} }
+ ],
+ has_area => $backend->{coverage}{area} ? 1 : 0,
+ type => 'HAFAS',
+ hafas => 1,
+ }
+ );
+ }
+
+ $self->render(
+ 'select_backend',
+ backends => \@backends,
+ hide_opts => 1,
+ hide_footer => 1
+ );
+}
+
sub autocomplete {
my ($self) = @_;
@@ -1900,11 +3008,28 @@ sub redirect_to_station {
}
}
- if ( $input =~ m{ ^ [a-zA-Z]{1,5} \s+ \d+ $ }x ) {
+ if ( $input =~ m{ ^ [a-zA-Z]{1,5} \s+ \d+ }x ) {
+ if ( $input =~ s{ \s* @ \s* (?<date> [0-9.]+) $ }{}x ) {
+ $params->param( date => $+{date} );
+ }
+ elsif ( $input =~ s{ \s* [(] \s* (?<date> [0-9.]+) \s* [)] $ }{}x ) {
+ $params->param( date => $+{date} );
+ }
$params = $params->to_string;
$self->redirect_to("/z/${input}?${params}");
}
+ elsif ( $params->param('efa') ) {
+ $params->remove('hafas');
+ $params = $params->to_string;
+ $self->redirect_to("/${input}?${params}");
+ }
+ elsif ( $params->param('hafas') and $params->param('hafas') ne '1' ) {
+ $params->remove('efa');
+ $params = $params->to_string;
+ $self->redirect_to("/${input}?${params}");
+ }
else {
+ $params->remove('efa');
my @candidates
= Travel::Status::DE::IRIS::Stations::get_station($input);
if (
diff --git a/lib/DBInfoscreen/Controller/Wagenreihung.pm b/lib/DBInfoscreen/Controller/Wagenreihung.pm
index 5d48705..b9f0ee3 100644
--- a/lib/DBInfoscreen/Controller/Wagenreihung.pm
+++ b/lib/DBInfoscreen/Controller/Wagenreihung.pm
@@ -10,147 +10,40 @@ use Mojo::Util qw(b64_encode b64_decode);
use utf8;
-use Travel::Status::DE::DBWagenreihung;
-use Travel::Status::DE::DBWagenreihung::Wagon;
+use Travel::Status::DE::DBRIS::Formation;
-sub get_zugbildung_db {
- my ( $self, $train_no ) = @_;
-
- say $train_no;
-
- my $details = $self->app->train_details_db->{$train_no};
-
- if ( not $details ) {
- return;
- }
-
- my @wagons;
-
- for my $wagon ( @{ $details->{wagons} } ) {
- my $wagon_type = $wagon->{type};
- my $wagon_number = $wagon->{number};
- my %wagon = (
- fahrzeugnummer => "",
- fahrzeugtyp => $wagon_type,
- kategorie => $wagon_type =~ m{^[0-9.]+$} ? 'LOK' : '',
- train_no => $train_no,
- wagenordnungsnummer => $wagon_number,
- positionamhalt => {
- startprozent => 0,
- endeprozent => 0,
- startmeter => 0,
- endemeter => 0,
- }
- );
- my $wagon = Travel::Status::DE::DBWagenreihung::Wagon->new(%wagon);
-
- if ( $details->{type} ) {
- $wagon->set_traintype( $details->{type} );
- }
- push( @wagons, $wagon );
- }
-
- my $pos = 0;
- for my $wagon (@wagons) {
- $wagon->{position}{start_percent} = $pos;
- $wagon->{position}{end_percent} = $pos + 5;
- $pos += 5;
- }
-
- my $train_type = $details->{rawType};
- $train_type =~ s{ - .* }{}x;
-
- my $route_start = $details->{route}{start} // $details->{route}{preStart};
- my $route_end = $details->{route}{end} // $details->{route}{postEnd};
- my $route = "${route_start} → ${route_end}";
-
- return {
- route => $route,
- train_type => $train_type,
- wagons => [@wagons]
- };
-}
-
-sub zugbildung_db {
- my ($self) = @_;
-
- my $train_no = $self->param('train');
-
- my $details = $self->get_zugbildung_db($train_no);
-
- if ( not $details ) {
- $self->render( 'not_found',
- message => "Keine Daten zu Zug ${train_no} bekannt" );
- return;
- }
+sub handle_wagenreihung_error {
+ my ( $self, $train, $err ) = @_;
$self->render(
- 'zugbildung_db',
- description => sprintf(
- 'Soll-Wagenreihung %s %s',
- $details->{train_type} // 'Zug', $train_no
- ),
- wr_error => undef,
- title => $details->{train_type} . ' ' . $train_no,
- route => $details->{route},
- zb => $details,
- train_no => $train_no,
- wagons => $details->{wagons},
+ 'wagenreihung',
+ title => $train,
+ wr_error => $err,
+ wr => undef,
+ wref => undef,
hide_opts => 1,
+ status => 500,
);
}
-sub handle_wagenreihung_error {
- my ( $self, $train_no, $err ) = @_;
-
- my $details = $self->get_zugbildung_db($train_no);
- if ( $details and @{ $details->{wagons} } ) {
- my $wr_error
- = "${err}. Ersatzweise werden die Solldaten laut Fahrplan angezeigt.";
- $self->render(
- 'zugbildung_db',
- description => sprintf(
- 'Soll-Wagenreihung %s %s',
- $details->{train_type} // 'Zug', $train_no
- ),
- wr_error => $wr_error,
- title => $details->{train_type} . ' ' . $train_no,
- route => $details->{route},
- zb => $details,
- train_no => $train_no,
- wagons => $details->{wagons},
- hide_opts => 1,
- );
- }
- else {
- $self->render(
- 'wagenreihung',
- title => "Zug $train_no",
- wr_error => $err,
- train_no => $train_no,
- wr => undef,
- wref => undef,
- hide_opts => 1,
- );
- }
-}
-
sub wagenreihung {
- my ($self) = @_;
- my $train = $self->stash('train');
- my $departure = $self->stash('departure');
+ my ($self) = @_;
my $exit_side = $self->param('e');
+ my $train_type = $self->param('category');
+ my $train_no = $self->param('number');
+ my $train = "${train_type} ${train_no}";
+
$self->render_later;
- $self->wagonorder->get_p( $train, $departure )->then(
+ $self->wagonorder->get_p( param => $self->req->query_params->to_hash )
+ ->then(
sub {
my ($json) = @_;
my $wr;
eval {
$wr
- = Travel::Status::DE::DBWagenreihung->new(
- from_json => $json );
+ = Travel::Status::DE::DBRIS::Formation->new( json => $json );
};
if ($@) {
$self->handle_wagenreihung_error( $train, scalar $@ );
@@ -158,8 +51,8 @@ sub wagenreihung {
}
if ( $exit_side and $exit_side =~ m{^a} ) {
- if ( $wr->sections and defined $wr->direction ) {
- my $section_0 = ( $wr->sections )[0];
+ if ( $wr->sectors and defined $wr->direction ) {
+ my $section_0 = ( $wr->sectors )[0];
my $direction = $wr->direction;
if ( $section_0->name eq 'A' and $direction == 0 ) {
$exit_side =~ s{^a}{};
@@ -179,22 +72,21 @@ sub wagenreihung {
my $wref = {
e => $exit_side ? substr( $exit_side, 0, 1 ) : '',
tt => $wr->train_type,
- tn => $train,
- s => $wr->station_name,
+ tn => $train_no,
p => $wr->platform
};
- if ( $wr->has_bad_wagons ) {
+ #if ( $wr->has_bad_wagons ) {
- # create fake positions as the correct ones are not available
- my $pos = 0;
- for my $wagon ( $wr->wagons ) {
- $wagon->{position}{start_percent} = $pos;
- $wagon->{position}{end_percent} = $pos + 4;
- $pos += 4;
- }
- }
- elsif ( defined $wr->direction and scalar $wr->wagons > 2 ) {
+ # # create fake positions as the correct ones are not available
+ # my $pos = 0;
+ # for my $wagon ( $wr->wagons ) {
+ # $wagon->{position}{start_percent} = $pos;
+ # $wagon->{position}{end_percent} = $pos + 4;
+ # $pos += 4;
+ # }
+ #}
+ if ( defined $wr->direction and scalar $wr->carriages > 2 ) {
# wagenlexikon images only know one orientation. They assume
# that the second class (i.e., the wagon with the lowest
@@ -208,17 +100,17 @@ sub wagenreihung {
# order differs, we do not show a direction, as we do not
# handle that case yet.
- my @wagons = $wr->wagons;
+ my @wagons = $wr->carriages;
# skip first/last wagon as it may be a locomotive
my $wna1 = $wagons[1]->number;
my $wna2 = $wagons[2]->number;
my $wnb1 = $wagons[-3]->number;
my $wnb2 = $wagons[-2]->number;
- my $wpa1 = $wagons[1]{position}{start_percent};
- my $wpa2 = $wagons[2]{position}{start_percent};
- my $wpb1 = $wagons[-3]{position}{start_percent};
- my $wpb2 = $wagons[-2]{position}{start_percent};
+ my $wpa1 = $wagons[1]->start_percent;
+ my $wpa2 = $wagons[2]->start_percent;
+ my $wpb1 = $wagons[-3]->start_percent;
+ my $wpb2 = $wagons[-2]->start_percent;
if ( $wna1 =~ m{^\d+$}
and $wna2 =~ m{^\d+$}
@@ -226,10 +118,10 @@ sub wagenreihung {
and $wnb2 =~ m{^\d+$} )
{
- # We need to perform normalization in two cases:
- # * wagon 1 is leftmost and its number is higher than wagon 2
- # * wagon 1 is rightmost and its number is lower than wagon 2
- # (-> the leftmost wagon has the highest number)
+ # We need to perform normalization in two cases:
+ # * wagon 1 is leftmost and its number is higher than wagon 2
+ # * wagon 1 is rightmost and its number is lower than wagon 2
+ # (-> the leftmost wagon has the highest number)
# However, if wpa/wna und wpb/wnb do not match, we have a
# winged train with different normalization requirements
@@ -269,33 +161,29 @@ sub wagenreihung {
$wref = b64_encode( encode_json($wref) );
- my $title = join( ' / ',
- map { $wr->train_type . ' ' . $_ } $wr->train_numbers );
+ my $title = join( ' / ', map { $_->{name} } $wr->trains );
$self->render(
'wagenreihung',
- description => sprintf(
- 'Ist-Wagenreihung %s in %s',
- $title, $wr->station_name
- ),
- wr_error => undef,
- title => $title,
- train_no => $train,
- wr => $wr,
- wref => $wref,
- exit_dir => $exit_dir,
- hide_opts => 1,
+ description => sprintf( 'Ist-Wagenreihung %s', $title ),
+ wr_error => undef,
+ title => $title,
+ wr => $wr,
+ wref => $wref,
+ exit_dir => $exit_dir,
+ hide_opts => 1,
+ ts => $json->{ts},
);
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$self->handle_wagenreihung_error( $train,
- $err->{error}->{msg} // "Unbekannter Fehler" );
+ $err // "Unbekannter Fehler" );
return;
}
- )->wait;
+ )->wait;
}
@@ -333,15 +221,15 @@ sub wagen {
);
}
- my $title = "Wagen $wagon_id";
+ my $title = 'Wagen ' . $wagon_id;
if ( $wref->{tt} and $wref->{tn} ) {
$title = sprintf( '%s %s', $wref->{tt}, $wref->{tn} );
if ($wagon_no) {
- $title .= " Wagen $wagon_no";
+ $title .= ' Wagen ' . $wagon_no;
}
else {
- $title .= " Wagen $wagon_id";
+ $title .= ' Wagen ' . $wagon_id;
}
}
diff --git a/lib/DBInfoscreen/Helper/DBRIS.pm b/lib/DBInfoscreen/Helper/DBRIS.pm
new file mode 100644
index 0000000..e780213
--- /dev/null
+++ b/lib/DBInfoscreen/Helper/DBRIS.pm
@@ -0,0 +1,93 @@
+package DBInfoscreen::Helper::DBRIS;
+
+# Copyright (C) 2025 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+use DateTime;
+use Encode qw(decode encode);
+use Travel::Status::DE::DBRIS;
+use Mojo::JSON qw(decode_json);
+use Mojo::Promise;
+use Mojo::UserAgent;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ my $version = $opt{version};
+
+ $opt{header}
+ = { 'User-Agent' =>
+"dbf/${version} on $opt{root_url} +https://finalrewind.org/projects/db-fakedisplay"
+ };
+
+ return bless( \%opt, $class );
+
+}
+
+sub get_journey_p {
+ my ( $self, %opt ) = @_;
+
+ my $agent = $self->{user_agent};
+
+ if ( my $proxy = $ENV{DBFAKEDISPLAY_DBRIS_PROXY} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
+ return Travel::Status::DE::DBRIS->new_p(
+ journey => $opt{id},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $agent->request_timeout(10)
+ );
+}
+
+# Input: TripID
+# Output: Promise returning a Travel::Status::DE::DBRIS::Journey instance on success
+sub get_polyline_p {
+ my ( $self, %opt ) = @_;
+
+ my $trip_id = $opt{id};
+ my $promise = Mojo::Promise->new;
+
+ my $agent = $self->{user_agent};
+
+ if ( my $proxy = $ENV{DBFAKEDISPLAY_DBRIS_PROXY} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
+ Travel::Status::DE::DBRIS->new_p(
+ journey => $trip_id,
+ with_polyline => 1,
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $agent->request_timeout(10)
+ )->then(
+ sub {
+ my ($dbris) = @_;
+ my $journey = $dbris->result;
+
+ $promise->resolve($journey);
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->debug("DBRIS->new_p($trip_id) error: $err");
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+1;
diff --git a/lib/DBInfoscreen/Helper/EFA.pm b/lib/DBInfoscreen/Helper/EFA.pm
index 41035d4..0e7f7d7 100644
--- a/lib/DBInfoscreen/Helper/EFA.pm
+++ b/lib/DBInfoscreen/Helper/EFA.pm
@@ -13,7 +13,7 @@ use Encode qw(decode encode);
use Mojo::JSON qw(decode_json);
use Mojo::Promise;
use Mojo::Util qw(url_escape);
-use XML::LibXML;
+use Travel::Status::DE::EFA;
sub new {
my ( $class, %opt ) = @_;
@@ -29,6 +29,51 @@ sub new {
}
+sub get_polyline_p {
+ my ( $self, %opt ) = @_;
+
+ my $stopseq = $opt{stopseq};
+ my $service = $opt{service};
+ my $promise = Mojo::Promise->new;
+
+ Travel::Status::DE::EFA->new_p(
+ service => $service,
+ stopseq => $stopseq,
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(10)
+ )->then(
+ sub {
+ my ($efa) = @_;
+ my $journey = $efa->result;
+
+ $promise->resolve($journey);
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->debug("EFA->new_p($stopseq) error: $err");
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+sub get_coverage {
+ my ( $self, $service ) = @_;
+
+ my $service_definition = Travel::Status::DE::EFA::get_service($service);
+
+ if ( not $service_definition ) {
+ return {};
+ }
+
+ return $service_definition->{coverage}{area} // {};
+}
+
sub get_json_p {
my ( $self, $cache, $url ) = @_;
@@ -49,8 +94,7 @@ sub get_json_p {
if ( my $err = $tx->error ) {
$self->{log}->debug(
-"efa->get_json_p($url): HTTP $err->{code} $err->{message}"
- );
+ "efa->get_json_p($url): HTTP $err->{code} $err->{message}");
$cache->freeze( $url, { error => $err->{message} } );
$promise->reject(
"GET $url returned HTTP $err->{code} $err->{message}");
@@ -60,8 +104,7 @@ sub get_json_p {
my $res = $tx->res->json;
if ( not $res ) {
- $self->{log}
- ->debug("efa->get_json_p($url): empty response");
+ $self->{log}->debug("efa->get_json_p($url): empty response");
$promise->reject("GET $url returned empty response");
return;
}
diff --git a/lib/DBInfoscreen/Helper/HAFAS.pm b/lib/DBInfoscreen/Helper/HAFAS.pm
index 11878ff..e16bad8 100644
--- a/lib/DBInfoscreen/Helper/HAFAS.pm
+++ b/lib/DBInfoscreen/Helper/HAFAS.pm
@@ -7,13 +7,14 @@ package DBInfoscreen::Helper::HAFAS;
use strict;
use warnings;
use 5.020;
+use utf8;
use DateTime;
use Encode qw(decode encode);
use Travel::Status::DE::HAFAS;
use Mojo::JSON qw(decode_json);
use Mojo::Promise;
-use XML::LibXML;
+use Mojo::UserAgent;
sub new {
my ( $class, %opt ) = @_;
@@ -29,153 +30,19 @@ sub new {
}
-sub get_json_p {
- my ( $self, $cache, $url ) = @_;
+sub get_coverage {
+ my ( $self, $service ) = @_;
- my $promise = Mojo::Promise->new;
+ my $service_definition = Travel::Status::DE::HAFAS::get_service($service);
- if ( my $content = $cache->thaw($url) ) {
- return $promise->resolve($content);
+ if ( not $service_definition ) {
+ return {};
}
- $self->{log}->debug("get_json_p($url)");
-
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
- ->then(
- sub {
- my ($tx) = @_;
-
- if ( my $err = $tx->error ) {
- $self->{log}->warn(
- "hafas->get_json_p($url): HTTP $err->{code} $err->{message}"
- );
- $promise->reject(
- "GET $url returned HTTP $err->{code} $err->{message}");
- return;
- }
- my $body
- = encode( 'utf-8', decode( 'ISO-8859-15', $tx->res->body ) );
-
- $body =~ s{^TSLs[.]sls = }{};
- $body =~ s{;$}{};
- $body =~ s{&#x0028;}{(}g;
- $body =~ s{&#x0029;}{)}g;
-
- my $json = decode_json($body);
-
- if ( not $json ) {
- $self->{log}->debug("hafas->get_json_p($url): empty response");
- $promise->reject("GET $url returned empty response");
- return;
- }
-
- $cache->freeze( $url, $json );
-
- $promise->resolve($json);
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->{log}->warn("hafas->get_json_p($url): $err");
- $promise->reject($err);
- return;
- }
- )->wait;
-
- return $promise;
-}
-
-sub trainsearch_p {
- my ( $self, %opt ) = @_;
-
- my $base
- = 'https://reiseauskunft.bahn.de/bin/trainsearch.exe/dn?L=vs_json&start=yes&rt=1';
-
- if ( not $opt{date_yy} ) {
- my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- $opt{date_yy} = $now->strftime('%d.%m.%y');
- $opt{date_yyyy} = $now->strftime('%d.%m.%Y');
- }
-
- # IRIS reports trains with unknown type as type "-". HAFAS thinks otherwise
- # and prefers the type to be left out entirely in this case.
- $opt{train_req} =~ s{^- }{};
-
- my $promise = Mojo::Promise->new;
-
- $self->get_json_p( $self->{realtime_cache},
- "${base}&date=$opt{date_yy}&trainname=$opt{train_req}" )->then(
- sub {
- my ($trainsearch) = @_;
-
- # Fallback: Take first result
- my $result = $trainsearch->{suggestions}[0];
-
- # Try finding a result for the current date
- for my $suggestion ( @{ $trainsearch->{suggestions} // [] } ) {
-
- # Drunken API, sail with care. Both date formats are used interchangeably
- if (
- exists $suggestion->{depDate}
- and ( $suggestion->{depDate} eq $opt{date_yy}
- or $suggestion->{depDate} eq $opt{date_yyyy} )
- )
- {
- # Train numbers are not unique, e.g. IC 149 refers both to the
- # InterCity service Amsterdam -> Berlin and to the InterCity service
- # Koebenhavns Lufthavn st -> Aarhus. One workaround is making
- # requests with the stationFilter=80 parameter. Checking the origin
- # station seems to be the more generic solution, so we do that
- # instead.
- if ( $opt{train_origin}
- and $suggestion->{dep} eq $opt{train_origin} )
- {
- $result = $suggestion;
- last;
- }
- }
- }
-
- if ($result) {
-
- # The trip_id's date part doesn't seem to matter -- so far, HAFAS is
- # happy as long as the date part starts with a number. HAFAS-internal
- # tripIDs use this format (withouth leading zero for day of month < 10)
- # though, so let's stick with it.
- my $date_map = $opt{date_yyyy};
- $date_map =~ tr{.}{}d;
- $result->{trip_id} = sprintf( '1|%d|%d|%d|%s',
- $result->{id}, $result->{cycle},
- $result->{pool}, $date_map );
- $promise->resolve($result);
- }
- else {
- $self->{log}->warn(
- "hafas->trainsearch_p($opt{train_req}): train not found");
- $promise->reject("Zug $opt{train_req} nicht gefunden");
- }
-
- # do not propagate $promise->reject's return value to this promise.
- # Perl implicitly returns the last statement, so we explicitly return
- # nothing to avoid this.
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->{log}->warn("hafas->trainsearch_p($opt{train_req}): $err");
- $promise->reject($err);
-
- # do not propagate $promise->reject's return value to this promise
- return;
- }
- )->wait;
-
- return $promise;
+ return $service_definition->{coverage}{area} // {};
}
-sub get_route_timestamps_p {
+sub get_route_p {
my ( $self, %opt ) = @_;
my $promise = Mojo::Promise->new;
@@ -183,41 +50,73 @@ sub get_route_timestamps_p {
my $hafas_promise;
+ my $agent = $self->{user_agent};
+ if ( $opt{service} and $opt{service} eq 'PKP' ) {
+
+ # PKP needs proxying
+ $agent = Mojo::UserAgent->new;
+ }
+
if ( $opt{trip_id} ) {
$hafas_promise = Travel::Status::DE::HAFAS->new_p(
+ service => $opt{service} // 'ÖBB',
journey => {
id => $opt{trip_id},
},
+ language => $opt{language},
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(10)
+ user_agent => $agent->request_timeout(10)
);
}
elsif ( $opt{train} ) {
- $opt{date_yy} = $opt{train}->start->strftime('%d.%m.%y');
- $opt{date_yyyy} = $opt{train}->start->strftime('%d.%m.%Y');
$opt{train_req} = $opt{train}->type . ' ' . $opt{train}->train_no;
$opt{train_origin} = $opt{train}->origin;
}
else {
$opt{train_req} = $opt{train_type} . ' ' . $opt{train_no};
- $opt{date_yy} = $now->strftime('%d.%m.%y');
- $opt{date_yyyy} = $now->strftime('%d.%m.%Y');
}
- $hafas_promise //= $self->trainsearch_p(%opt)->then(
+ $hafas_promise //= Travel::Status::DE::HAFAS->new_p(
+ service => $opt{service} // 'ÖBB',
+ journeyMatch => $opt{train_req} =~ s{^- }{}r,
+ datetime => ( $opt{train} ? $opt{train}->start : $opt{datetime} ),
+ language => $opt{language},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $agent->request_timeout(10)
+ )->then(
sub {
- my ($trainsearch_result) = @_;
- my $trip_id = $trainsearch_result->{trip_id};
+ my ($hafas) = @_;
+ my @results = $hafas->results;
+
+ if ( not @results ) {
+ return Mojo::Promise->reject(
+ "journeyMatch($opt{train_req}) found no results");
+ }
+
+ my $result = $results[0];
+ if ( @results > 1 ) {
+ for my $journey (@results) {
+ if ( $opt{train_origin}
+ and ( $journey->route )[0]->loc->name eq
+ $opt{train_origin} )
+ {
+ $result = $journey;
+ last;
+ }
+ }
+ }
+
return Travel::Status::DE::HAFAS->new_p(
+ service => $opt{service} // 'ÖBB',
journey => {
- id => $trip_id,
-
- # name => $opt{train_no},
+ id => $result->id,
},
+ language => $opt{language},
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(10)
+ user_agent => $agent->request_timeout(10)
);
}
);
@@ -226,47 +125,136 @@ sub get_route_timestamps_p {
sub {
my ($hafas) = @_;
my $journey = $hafas->result;
- my $ret = {};
-
+ my @ret;
my $station_is_past = 1;
+
+ my $num_names = 0;
+ my $prev_name = q{};
+ my $num_directions = 0;
+ my $prev_direction = q{};
+ my $num_operators = 0;
+ my $prev_operator = q{};
+
for my $stop ( $journey->route ) {
- my $name = $stop->{name};
- $ret->{$name} = $ret->{ $stop->{eva} } = {
- name => $stop->{name},
- eva => $stop->{eva},
- sched_arr => $stop->{sched_arr},
- sched_dep => $stop->{sched_dep},
- rt_arr => $stop->{rt_arr},
- rt_dep => $stop->{rt_dep},
- arr_delay => $stop->{arr_delay},
- dep_delay => $stop->{dep_delay},
- arr_cancelled => $stop->{arr_cancelled},
- dep_cancelled => $stop->{dep_cancelled},
- platform => $stop->{platform},
- sched_platform => $stop->{sched_platform},
- load => $stop->{load},
- isCancelled => (
- ( $stop->{arr_cancelled} or not $stop->{sched_arr} )
- and
- ( $stop->{dep_cancelled} or not $stop->{sched_dep} )
- ),
- };
+ my $prod = $stop->prod_dep // $stop->prod_arr;
+ if ( $prod and $prod->name and $prod->name ne $prev_name ) {
+ $num_names++;
+ $prev_name = $prod->name;
+ }
+ if ( $prod
+ and $prod->operator
+ and $prod->operator ne $prev_operator )
+ {
+ $num_operators++;
+ $prev_operator = $prod->operator;
+ }
+ if ( $stop->direction and $stop->direction ne $prev_direction )
+ {
+ $num_directions++;
+ $prev_direction = $stop->direction;
+ }
+ }
+
+ $prev_name = q{};
+ $prev_direction = q{};
+ $prev_operator = q{};
+
+ for my $stop ( $journey->route ) {
+
+ my $prod = $stop->prod_dep // $stop->prod_arr;
+ my %annotation;
+ if ( $num_names > 1
+ and $prod
+ and $prod->name
+ and $prod->name ne $prev_name )
+ {
+ $prev_name = $annotation{prod_name} = $prod->name;
+ }
+ if ( $num_operators > 1
+ and $prod
+ and $prod->operator
+ and $prod->operator ne $prev_operator )
+ {
+ $prev_operator = $annotation{operator} = $prod->operator;
+ }
+ if ( $num_directions > 1
+ and $stop->direction
+ and $stop->direction ne $prev_direction )
+ {
+ $prev_direction = $annotation{direction} = $stop->direction;
+ }
+
+ if (%annotation) {
+ $annotation{is_annotated} = 1;
+ }
+
+ push(
+ @ret,
+ {
+ name => $stop->loc->name,
+ eva => $stop->loc->eva,
+ sched_arr => $stop->sched_arr,
+ sched_dep => $stop->sched_dep,
+ rt_arr => $stop->rt_arr,
+ rt_dep => $stop->rt_dep,
+ arr_delay => $stop->arr_delay,
+ dep_delay => $stop->dep_delay,
+ arr_cancelled => $stop->arr_cancelled,
+ dep_cancelled => $stop->dep_cancelled,
+ tz_offset => $stop->tz_offset,
+ platform => $stop->platform,
+ sched_platform => $stop->sched_platform,
+ load => $stop->load,
+ isAdditional => $stop->is_additional,
+ isCancelled => (
+ ( $stop->arr_cancelled or not $stop->sched_arr )
+ and
+ ( $stop->dep_cancelled or not $stop->sched_dep )
+ ),
+ %annotation,
+ }
+ );
if (
$station_is_past
- and not $ret->{$name}{isCancelled}
+ and not $ret[-1]{isCancelled}
and $now->epoch < (
- $ret->{$name}{rt_arr} // $ret->{$name}{rt_dep}
- // $ret->{$name}{sched_arr}
- // $ret->{$name}{sched_dep} // $now
+ $ret[-1]{rt_arr} // $ret[-1]{rt_dep}
+ // $ret[-1]{sched_arr} // $ret[-1]{sched_dep} // $now
)->epoch
)
{
$station_is_past = 0;
}
- $ret->{$name}{isPast} = $station_is_past;
+ $ret[-1]{isPast} = $station_is_past;
+ if ( $stop->tz_offset ) {
+ if ( $stop->sched_arr ) {
+ $ret[-1]{local_sched_arr}
+ = $stop->sched_arr->clone->add(
+ minutes => $stop->tz_offset );
+ }
+ if ( $stop->sched_dep ) {
+ $ret[-1]{local_sched_dep}
+ = $stop->sched_dep->clone->add(
+ minutes => $stop->tz_offset );
+ }
+ if ( $stop->rt_arr ) {
+ $ret[-1]{local_rt_arr} = $stop->rt_arr->clone->add(
+ minutes => $stop->tz_offset );
+ }
+ if ( $stop->rt_dep ) {
+ $ret[-1]{local_rt_dep} = $stop->rt_dep->clone->add(
+ minutes => $stop->tz_offset );
+ }
+ $ret[-1]{local_dt_ad} = $ret[-1]{local_rt_arr}
+ // $ret[-1]{local_sched_arr} // $ret[-1]{local_rt_dep}
+ // $ret[-1]{local_sched_dep};
+ $ret[-1]{local_dt_da} = $ret[-1]{local_rt_dep}
+ // $ret[-1]{local_sched_dep} // $ret[-1]{local_rt_arr}
+ // $ret[-1]{local_sched_arr};
+ }
}
- $promise->resolve( $ret, $journey );
+ $promise->resolve( \@ret, $journey, $hafas );
return;
}
)->catch(
@@ -283,11 +271,22 @@ sub get_route_timestamps_p {
# Input: (HAFAS TripID, line number)
# Output: Promise returning a Travel::Status::DE::HAFAS::Journey instance on success
sub get_polyline_p {
- my ( $self, $trip_id, $line ) = @_;
+ my ( $self, %opt ) = @_;
+ my $trip_id = $opt{id};
+ my $line = $opt{line};
+ my $service = $opt{service} // 'ÖBB';
my $promise = Mojo::Promise->new;
+ my $agent = $self->{user_agent};
+ if ( $opt{service} and $opt{service} eq 'PKP' ) {
+
+ # PKP needs proxying
+ $agent = Mojo::UserAgent->new;
+ }
+
Travel::Status::DE::HAFAS->new_p(
+ service => $service,
journey => {
id => $trip_id,
name => $line,
@@ -295,7 +294,7 @@ sub get_polyline_p {
with_polyline => 1,
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(10)
+ user_agent => $agent->request_timeout(10)
)->then(
sub {
my ($hafas) = @_;
diff --git a/lib/DBInfoscreen/Helper/MOTIS.pm b/lib/DBInfoscreen/Helper/MOTIS.pm
new file mode 100644
index 0000000..002a601
--- /dev/null
+++ b/lib/DBInfoscreen/Helper/MOTIS.pm
@@ -0,0 +1,82 @@
+package DBInfoscreen::Helper::MOTIS;
+
+# Copyright (C) 2025 networkException <git@nwex.de>
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+use DateTime;
+use Encode qw(decode encode);
+use Travel::Status::MOTIS;
+use Mojo::JSON qw(decode_json);
+use Mojo::Promise;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ my $version = $opt{version};
+
+ $opt{header}
+ = { 'User-Agent' =>
+"dbf/${version} on $opt{root_url} +https://finalrewind.org/projects/db-fakedisplay"
+ };
+
+ return bless( \%opt, $class );
+
+}
+
+sub get_coverage {
+ my ( $self, $service ) = @_;
+
+ my $service_definition = Travel::Status::MOTIS::get_service($service);
+
+ if ( not $service_definition ) {
+ return {};
+ }
+
+ return $service_definition->{coverage}{area} // {};
+}
+
+# Input: TripID
+# Output: Promise returning a Travel::Status::MOTIS::Trip instance on success
+sub get_polyline_p {
+ my ( $self, %opt ) = @_;
+
+ my $trip_id = $opt{id};
+ my $service = $opt{service} // 'transitous';
+
+ my $promise = Mojo::Promise->new;
+
+ my $agent = $self->{user_agent};
+
+ Travel::Status::MOTIS->new_p(
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $agent->request_timeout(10),
+
+ service => $service,
+ trip_id => $trip_id,
+ )->then(
+ sub {
+ my ($motis) = @_;
+ my $trip = $motis->result;
+
+ $promise->resolve($trip);
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->debug("MOTIS->new_p($trip_id) error: $err");
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+1;
diff --git a/lib/DBInfoscreen/Helper/Wagonorder.pm b/lib/DBInfoscreen/Helper/Wagonorder.pm
index d59df14..9981244 100644
--- a/lib/DBInfoscreen/Helper/Wagonorder.pm
+++ b/lib/DBInfoscreen/Helper/Wagonorder.pm
@@ -8,6 +8,7 @@ use strict;
use warnings;
use 5.020;
+use DateTime;
use Mojo::Promise;
sub new {
@@ -24,181 +25,51 @@ sub new {
}
-sub is_available_p {
- my ( $self, $train, $wr_link ) = @_;
- my $promise = Mojo::Promise->new;
-
- $self->check_wagonorder_p( $train->train_no, $wr_link )->then(
- sub {
- my ($body) = @_;
- $promise->resolve($body);
- return;
- },
- sub {
- if ( $train->is_wing ) {
- my $wing = $train->wing_of;
- return $self->check_wagonorder_p( $wing->train_no, $wr_link );
- }
- else {
- $promise->reject;
- return;
- }
- }
- )->then(
- sub {
- my ($body) = @_;
- $promise->resolve($body);
- return;
- },
- sub {
- $promise->reject;
- return;
- }
- )->wait;
-
- return $promise;
-}
-
-sub get_dbdb_p {
- my ( $self, $url ) = @_;
-
- my $promise = Mojo::Promise->new;
+sub get_p {
+ my ( $self, %opt ) = @_;
- my $cache = $self->{main_cache};
+ my %param;
- if ( my $content = $cache->get($url) ) {
- if ($content) {
- return $promise->resolve($content);
- }
- else {
- return $promise->reject;
- }
+ if ( $opt{param} ) {
+ %param = %{ $opt{param} };
+ delete $param{e};
}
-
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
- ->then(
- sub {
- my ($tx) = @_;
- if ( $tx->result->is_success ) {
- my $body = $tx->result->body;
- $cache->set( $url, $body );
- $promise->resolve($body);
- }
- else {
- $cache->set( $url, q{} );
- $promise->reject;
- }
- return;
- }
- )->catch(
- sub {
- $cache->set( $url, q{} );
- $promise->reject;
- return;
- }
- )->wait;
- return $promise;
-}
-
-sub head_dbdb_p {
- my ( $self, $url ) = @_;
-
- my $promise = Mojo::Promise->new;
-
- my $cache = $self->{main_cache};
-
- if ( my $content = $cache->get($url) ) {
- $self->{log}->debug("wagonorder->head_dbdb_p($url): cached ($content)");
- if ( $content eq 'y' ) {
- return $promise->resolve;
- }
- else {
- return $promise->reject;
- }
+ else {
+ my $datetime = $opt{datetime}->clone->set_time_zone('UTC');
+ %param = (
+ administrationId => 80,
+ category => $opt{train_type},
+ date => $datetime->strftime('%Y-%m-%d'),
+ evaNumber => $opt{eva},
+ number => $opt{train_number},
+ time => $datetime->rfc3339 =~ s{(?=Z)}{.000}r
+ );
}
- $self->{user_agent}->request_timeout(5)->head_p( $url => $self->{header} )
- ->then(
- sub {
- my ($tx) = @_;
- if ( $tx->result->is_success ) {
- $self->{log}->debug("wagonorder->head_dbdb_p($url): y");
- $cache->set( $url, 'y' );
- $promise->resolve;
- }
- else {
- $self->{log}->debug("wagonorder->head_dbdb_p($url): n");
- $cache->set( $url, 'n' );
- $promise->reject;
- }
- return;
- }
- )->catch(
- sub {
- $self->{log}->debug("wagonorder->head_dbdb_p($url): n");
- $cache->set( $url, 'n' );
- $promise->reject;
- return;
- }
- )->wait;
- return $promise;
-}
-
-sub has_cycle_p {
- my ( $self, $train_no ) = @_;
-
- return $self->head_dbdb_p(
- "https://lib.finalrewind.org/dbdb/db_umlauf/${train_no}.svg");
-}
-
-sub check_wagonorder_p {
- my ( $self, $train_no, $wr_link ) = @_;
+ my $url = sprintf( '%s?%s',
+'https://www.bahn.de/web/api/reisebegleitung/wagenreihung/vehicle-sequence',
+ join( '&', map { $_ . '=' . $param{$_} } sort keys %param ) );
my $promise = Mojo::Promise->new;
- $self->head_dbdb_p(
- "https://lib.finalrewind.org/dbdb/has_wagonorder/${train_no}/${wr_link}"
- )->then(
- sub {
- $promise->resolve;
- return;
- }
- )->catch(
- sub {
- $self->get_p( $train_no, $wr_link )->then(
- sub {
- $promise->resolve;
- return;
- }
- )->catch(
- sub {
- $promise->reject;
- return;
- }
- )->wait;
- return;
+ if ( my $content = $self->{main_cache}->thaw($url) ) {
+ $self->{log}->debug("wagonorder->get_p($url): cached");
+ if ( $content->{error} ) {
+ return $promise->reject(
+"GET $url: HTTP $content->{error}{code} $content->{error}{message} (cachd)"
+ );
}
- )->wait;
-
- return $promise;
-}
-
-sub get_p {
- my ( $self, $train_no, $api_ts ) = @_;
-
- my $url
- = "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}";
-
- my $cache = $self->{realtime_cache};
-
- my $promise = Mojo::Promise->new;
+ return $promise->resolve( $content, \%param );
+ }
- if ( my $content = $cache->thaw($url) ) {
+ if ( my $content = $self->{realtime_cache}->thaw($url) ) {
$self->{log}->debug("wagonorder->get_p($url): cached");
if ( $content->{error} ) {
- return $promise->reject($content);
+ return $promise->reject(
+"GET $url: HTTP $content->{error}{code} $content->{error}{message} (cachd)"
+ );
}
- return $promise->resolve($content);
+ return $promise->resolve( $content, \%param );
}
$self->{user_agent}->request_timeout(10)->get_p( $url => $self->{header} )
@@ -216,16 +87,18 @@ sub get_p {
$self->{log}->debug(
"wagonorder->get_p($url): HTTP $err->{code} $err->{message}"
);
- $cache->freeze( $url, $json );
- $promise->reject($json);
+ $self->{realtime_cache}->freeze( $url, $json );
+ $promise->reject("GET $url: HTTP $err->{code} $err->{message}");
return;
}
$self->{log}->debug("wagonorder->get_p($url): OK");
my $json = $tx->res->json;
+ $json->{ts} = DateTime->now( time_zone => 'Europe/Berlin' )
+ ->strftime('%d.%m.%Y %H:%M');
- $cache->freeze( $url, $json );
- $promise->resolve($json);
+ $self->{main_cache}->freeze( $url, $json );
+ $promise->resolve( $json, \%param );
return;
}
)->catch(
diff --git a/lib/DBInfoscreen/I18N/en.pm b/lib/DBInfoscreen/I18N/en.pm
new file mode 100644
index 0000000..3abb70f
--- /dev/null
+++ b/lib/DBInfoscreen/I18N/en.pm
@@ -0,0 +1,84 @@
+package DBInfoscreen::I18N::en;
+
+# Copyright (C) 2023 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use Mojo::Base 'DBInfoscreen::I18N';
+
+our %Lexicon = (
+
+ # common
+ 'Stationen in der Umgebung suchen' => 'Find stops nearby',
+
+ # layouts/app
+ 'Mehrdeutige Eingabe' => 'Ambiguous input',
+ 'Bitte eine Station aus der Liste auswählen' =>
+ 'Please select a station from the list',
+ 'Zug / Station' => 'Enter train number or station name',
+ 'Zug, Stationsname oder Ril100-Kürzel' =>
+ 'train, station name, or DS100 code',
+ 'Abfahrtstafel' => 'Show departures',
+ 'Weitere Einstellungen' => 'Preferences',
+ 'Zeiten inkl. Verspätung angeben' => 'Include delay in timestamps',
+ 'Verspätungen erst ab 5 Minuten anzeigen' => 'Hide delays below 5 minutes',
+ 'Mehr Details' => 'Verbose mode',
+'Betriebliche Bahnhofstrennungen berücksichtigen (z.B. "Hbf (Fern+Regio)" vs. "Hbf (S)")'
+ => 'Respect split stations; do not join them',
+ 'Bereits abgefahrene Züge anzeigen' => 'Include past trains',
+ 'Formular verstecken' => 'Hide form',
+ 'Nur Züge über' => 'Only show trains via',
+ 'Bahnhof 1, Bhf2, ... (oder regulärer Ausdruck)' =>
+ 'Station 1, 2, ... (or regular expression)',
+ 'Gleise' => 'Platforms',
+ 'Ankunfts- oder Abfahrtszeit anzeigen?' => 'Show arrival or departure?',
+ 'Abfahrt bevorzugen' => 'prefer departure',
+ 'Nur Abfahrt' => 'departure only',
+ 'Nur Ankunft' => 'arrival only',
+ 'Anzeigen' => 'Submit',
+ 'Datenschutz' => 'Privacy',
+ 'Impressum' => 'Imprint',
+
+ # landing page
+ 'Oder hier angeben:' => 'Or enter manually:',
+
+ # train details
+ 'Gleis' => 'Platform',
+ 'An:' => 'Arr',
+ 'Ab:' => 'Dep',
+ 'Plan:' => 'Sched',
+ 'Auslastung unbekannt' => 'Occupancy unknown',
+ 'Geringe Auslastung' => 'Low occupancy',
+ 'Hohe Auslastung' => 'High occupancy',
+ 'Sehr hohe Auslastung' => 'Very high occupancy',
+ 'Zug ist ausgebucht' => 'Fully booked',
+ 'Geringe Auslastung erwartet' => 'Low occupancy expected',
+ 'Hohe Auslastung erwartet' => 'High occupancy expected',
+ 'Sehr hohe Auslastung erwartet' => 'Very high occupancy expected',
+ 'Meldungen' => 'Messages',
+ 'Fahrtverlauf am' => 'Route on',
+ 'Betrieb' => 'Operator',
+ 'Karte' => 'Map',
+ 'Wagen' => 'Composition',
+
+ # wagon order
+ 'Nach' => 'To',
+ 'in Abschnitt' => 'in sections',
+ 'Wagen ' => 'carriage ',
+
+ # map
+ 'Fahrt' => 'Trip',
+ 'von' => 'from',
+ 'nach' => 'to',
+ 'Nächster Halt:' => 'Next stop:',
+ 'um' => 'at',
+ 'auf Gleis' => 'on platform',
+ 'Aufenthalt in' => 'Stopped in',
+ 'an Gleis' => 'on platform',
+ 'bis' => 'until',
+ 'Abfahrt in' => 'Departs',
+ 'von Gleis' => 'from platform',
+ 'Endstation erreicht um' => 'Terminus reached at',
+);
+
+1;
diff --git a/public/static/css/dark.min.css b/public/static/css/dark.min.css
index fcd43d7..3809a85 100644
--- a/public/static/css/dark.min.css
+++ b/public/static/css/dark.min.css
@@ -1 +1 @@
-body{margin:0;color:#fff;background-color:#101010}html{font-family:"Arimo", "Arial", Sans-Serif}a{color:#99f;text-decoration:none}.visually-hidden{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}p,div.about,div.input-field,div.notes{max-width:94%;margin-left:auto;margin-right:auto}p{text-align:justify}div.content{width:100%;margin:0}.copyright{margin-top:1em;color:#999;clear:both}.wagonorder{position:relative;width:100%;height:100ex}.wagonorder.exit-unknown .section{left:1em;width:2em}.wagonorder.exit-unknown .wagon{left:3em;min-width:6em}.wagonorder.exit-unknown .details{left:10em;right:0em}.wagonorder.exit-left .section{left:1em;width:2em;background-color:#222}.wagonorder.exit-left .wagon{left:3em;min-width:6em}.wagonorder.exit-left .details{left:10em;right:0em}.wagonorder.exit-right .section{right:1em;width:2em;background-color:#222}.wagonorder.exit-right .wagon{right:3em;min-width:6em}.wagonorder.exit-right .details{right:10em;left:0em;text-align:right}.wagonorder .section{position:absolute;text-align:center}.wagonorder .wagon{position:absolute;border:1px solid #999;padding-left:0.2em;padding-right:0.2em}.wagonorder .wagon .material-icons{color:#bbb}.wagonorder .wagon .direction{position:absolute;left:0.2em;bottom:0;right:0;text-align:center;color:#bbb}.wagonorder .wagon~.wagon{border-top:none}.wagonorder .firstclass{background-color:#330}.wagonorder .powercar{background-color:#222}.wagonorder .nondestwagon{border-style:dashed}.wagonorder .details{position:absolute;padding-top:0.5ex}.wagonorder .details .type{display:inline-block;width:5em;color:#bbb}.wagonorder .details a.type{color:#99f}.wagonorder .details .uicunknown{color:#999}.wagonorder .details .uicexchange{margin-right:0.2em;color:#999}.wagonorder .details .uiccountry{margin-right:0.2em;color:#999}.wagonorder .details .uic5{margin-right:0.2em;color:#999}.wagonorder .details .uic56{color:#bbb;font-weight:bold}.wagonorder .details .uic78{margin-right:0.2em;color:#bbb;font-weight:bold}.wagonorder .details .uic78::before{content:"-"}.wagonorder .details .uictype{margin-right:0.2em;color:#bbb;font-weight:bold}.wagonorder .details .uicno{color:#bbb}.wagonorder .details .uiccheck{color:#999}.wagonorder .details .uiccheck::before{content:"-"}.singlewagon .sign-left{float:left;padding-left:5%}.singlewagon .sign-right{float:right;padding-right:5%}.singlewagon .sign-center{text-align:center}.singlewagon .platform{text-align:center;background-color:#444;font-weight:bold;padding-top:0.5em;padding-bottom:0.5em}.singlewagon img.wagonfile{width:100%;margin-top:0.2em;margin-bottom:0.2em}div.app{border-width:1px 2px;width:100%;margin-bottom:5em}div.app>ul{position:relative;width:100%;list-style-type:none;margin:0;padding:0}div.app>ul>li{min-height:7em;display:block;width:100%;position:relative;border-bottom:1px solid #999;background-color:#101010}div.app>ul>li.cancelled{background-color:#512f00}div.app>ul>li.past{opacity:0.8;background-color:#222}div.app>ul>li>a{color:#fff}div.app>ul>li .anchor{position:relative;top:-12em}div.app>ul>li .line{font-size:2.7em;position:absolute;bottom:5px;left:2px;max-width:6em;max-height:3ex;overflow:hidden}div.app>ul>li .line .trainno{font-weight:normal}div.app>ul>li .line .trainno_sub{font-weight:normal;font-size:0.6em;text-align:center;margin-top:-0.2em}div.app>ul>li .sbahn .trainno_sub{font-weight:normal;font-size:0.5em;text-align:center;margin-top:-0.25em}div.app>ul>li .lineinfo{color:#fff;font-size:2em;position:absolute;top:0px;left:2px}div.app>ul>li .route,div.app>ul>li .info{background-color:transparent;font-size:2.1em;position:absolute;top:0;left:7.7em;right:7em;height:1.5em;overflow:hidden}div.app>ul>li .route{color:#ddd}div.app>ul>li .info{color:#f77}div.app>ul>li .dest,div.app>ul>li .origin{background-color:transparent;font-size:4em;position:absolute;bottom:0;left:4em;width:70%;overflow:hidden;color:#fff}div.app>ul>li .dest{background-color:transparent;color:#fff}div.app>ul>li .origin{background-color:transparent;color:#bbb}div.app>ul>li .origin:before{content:"von "}div.app>ul>li .platform{background-color:transparent;font-size:3em;font-weight:bold;position:absolute;right:5px;bottom:0;padding-left:0.2em;color:#fff}div.app>ul>li .changed-platform{color:#f77}div.app>ul>li .time{background-color:transparent;font-size:2.3em;position:absolute;right:5px;top:1px;padding-left:0.2em;color:#fff}div.app>ul>li .time.delayed{color:#f77;background-color:transparent}div.app>ul>li .time .no-realtime{background-color:transparent;padding-right:1ex}div.app>ul>li .time .no-realtime i.material-icons{font-size:12px}div.app>ul>li .time .delay{font-size:1em;color:#f77;background-color:transparent;padding-right:1ex}div.app>ul>li .time .undelay{font-size:1em;color:#7f7;padding-right:1ex}div.app>ul>li .time .delaynorm{font-size:0.9em;color:#d99}div.app>ul>li .time .undelaynorm{font-size:0.9em;color:#9d9}div.app .trainsubtype{font-weight:normal;font-size:70%;position:relative;vertical-align:baseline;top:-0.6ex;left:-0.5ex}div.app .replacement{color:#afa}div.app .replaced{color:#faa}div.app .sbahn{font-weight:bold;border-radius:30px;padding:3px 6px 2px 6px;background-color:#151}div.app .bahn,div.app .fern,div.app .ext{font-weight:bold;border-radius:5px;padding:3px 5px 2px 5px}div.app .bahn{background-color:#333}div.app .fern{background-color:#511}div.app .ext{border:2px solid #333}div.app .tram,div.app .bus,div.app .ubahn{padding:3px 5px 2px 5px}div.app .tram{background-color:#411}div.app .bus{background-color:#515}div.app .ubahn{background-color:#071e62}div.app .moreinfo{font-size:2.1em;position:fixed;left:0;right:0;bottom:0em;z-index:5;overflow:auto;cursor:default;background-color:#101010}div.app .moreinfo .mheader,div.app .moreinfo .mfooter{max-width:50em;margin-left:auto;margin-right:auto}div.app .moreinfo .mheader{text-align:center;font-size:120%;padding-top:0.5em;padding-bottom:0.5em;padding-left:1em;padding-right:1em;border-bottom:0.1em dashed #cccccc}div.app .moreinfo .mfooter{padding-top:0.5em;padding-left:1em;padding-right:1em}div.app .moreinfo .dataline{font-size:120%;width:100%;display:flex;justify-content:space-between;margin-bottom:1em}div.app .moreinfo .dataline>div{width:33%}div.app .moreinfo .departure{text-align:right}div.app .moreinfo .platform{text-align:center}div.app .moreinfo .arrival{display:inline-block;text-align:right}div.app .moreinfo .loading{text-align:center;width:100%;color:#888888}div.app .moreinfo .minfo{color:#f77}div.app .moreinfo .timehidden{color:#bbb}div.app .moreinfo .undelay{color:#7f7}div.app .moreinfo .verbose{margin-bottom:1em}div.app .moreinfo .verbose .no-realtime{color:#f77}div.app .moreinfo .messages i.material-icons{font-size:14px}div.app .moreinfo .details{margin-top:1em}div.app .moreinfo .mroute .important-stop{color:#fff}div.app .moreinfo .mroute .generic-stop{color:#bbb}div.app .moreinfo .mroute .additional-stop{color:#7f7}div.app .moreinfo .mroute .cancelled-stop{color:#f77}div.app .moreinfo .mroute .past-stop{list-style-type:disc}div.app .moreinfo .mroute .future-stop{list-style-type:circle}div.app .moreinfo .mroute i.material-icons{font-size:14px}div.app .moreinfo .db-attr{margin-bottom:1em}div.app .moreinfo .db-attr span{margin-right:0.5em}div.app .collapsed-moreinfo{display:none}div.app .expanded-moreinfo{display:block}ul.ui-autocomplete{max-height:20em;overflow-x:hidden;overflow-y:auto}div.geolocation{text-align:center}div.candidatestatus{text-align:center;color:#999999}div.candidatelist a{display:block;text-decoration:none;font-size:1.4em;padding-top:0.3em;text-align:center;border-bottom:1px solid #999999}div.candidatelist a .distance:after{content:" km"}div.candidatelist a .distance{font-size:0.6em;color:#999999;padding-top:0.2em;padding-bottom:0.3em}div.candidatelist a .traininfo{font-size:0.7em;color:#999999;padding-top:0.2em;padding-bottom:0.3em}div.about{margin-top:2em;font-family:Sans-Serif;color:#bbb}div.about a{color:#99f;text-decoration:none}.notice{padding:15px;margin-bottom:20px;border:1px solid #bce8f1;border-radius:4px;color:#31708f;background-color:#d9edf7;margin-left:auto;margin-right:auto}.warning{padding:15px;margin-bottom:20px;border:1px solid #faebcc;border-radius:4px;color:#8a6d3b;background-color:#fcf8e3;margin-left:auto;margin-right:auto}.error{padding:15px;margin-bottom:20px;border:1px solid #ebccd1;border-radius:4px;color:#a94442;background-color:#f2dede;margin-left:auto;margin-right:auto}.error .errcode{font-family:Monospace;margin-top:2em;font-size:100%;color:#aaaaaa}.container{max-width:60em;margin-left:auto;margin-right:auto}pre{margin-bottom:2em}span.optional,span.notes{color:#bbb}.moresettings-header{cursor:pointer}.moresettings-header-collapsed:before{content:"▹ "}.moresettings-header-expanded:before{content:"▿ "}.moresettings-collapsed{display:none}.moresettings-expanded{display:block}.developers-header{cursor:pointer}.developers-header-collapsed:before{content:"▹ "}.developers-header-expanded:before{content:"▿ "}.developers-collapsed{display:none}.developers-expanded{display:block}div.break{height:1em}div.field{margin-top:0.3em;margin-bottom:0.6em}.disabledbutton{display:inline-block;vertical-align:baseline;border-radius:4px;border:1px solid #cccccc;box-shadow:none;padding:0.9ex;margin-right:1em}.smallbutton{display:inline-block;vertical-align:baseline;border-radius:4px;border:1px solid #2e6da4;transition:background-color .3s;color:#fff;background-color:#337ab7;cursor:pointer;box-shadow:none;padding:0.9ex;margin-right:1em}.smallbutton .material-icons,.disabledbutton .material-icons{display:block;float:left;margin-right:0.5ex}.smallbutton img{display:block;float:left;margin-right:0.7ex;height:1.2em}input,select,.button{display:inline-block;width:60em;max-width:100%;min-height:1.8em;border-radius:4px;color:#fff;background-color:#101010;border:1px solid #444;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);font-size:90%;text-align:center;vertical-align:middle}input[type="text"]{width:59em;padding-left:0.5em;padding-right:0.5em;text-align:left;box-sizing:border-box}select{min-height:2em}input[type="checkbox"]{width:1.5em;box-shadow:none}input[type="submit"],.button{transition:background-color .3s;color:#fff;background-color:#337ab7;border-color:#2e6da4;cursor:pointer;box-shadow:none;padding-top:0.9ex;padding-bottom:0.9ex}.button{padding-top:1.1ex;padding-bottom:0}input[type="submit"]:active,input[type="submit"]:focus,input[type="submit"]:hover,.button:active,.button:focus,.button:hover,.smallbutton:active,.smallbutton:focus,.smallbutton:hover{color:#fff;background-color:#286090;border-color:#204d74}input[type="submit"]:active,.button:active{box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.button-light{color:#ddd;background-color:#101010;border-color:#444}.button-light:active,.button-light:focus,.button-light:hover{color:#ddd;background-color:#111;border-color:#333}div.notes{margin-top:2em}div.notes ul{margin-top:1em}div.app{max-width:60em;margin-left:auto;margin-right:auto}.navbar-fixed{position:relative;z-index:997}.navbar-fixed nav{position:fixed}nav{box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}nav{width:100%;overflow:hidden}nav a{color:#fff}nav .nav-wrapper{position:relative;height:100%}nav i,nav i.material-icons{display:block;font-size:24px}nav .brand-logo{position:absolute;display:inline-block;padding-left:0.5rem}nav ul{margin:0;padding-left:0;list-style-type:none}nav ul li{transition:background-color .3s;float:left;padding:0;list-style-type:none;background-color:#00838f}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}@media only screen and (max-width: 600px){div.app>ul>li{font-size:35%}div.navbar-fixed{height:56px}.moreinfo{top:56px}nav{height:56px;line-height:56px}nav .brand-logo{font-size:1.5rem}nav .nav-wrapper i{height:56px;line-height:56px}}@media only screen and (min-width: 600px){div.app>ul>li{font-size:40%}div.navbar-fixed{height:64px}.moreinfo{top:64px}nav{height:64px;line-height:64px}nav .brand-logo{font-size:2.1rem}nav .nav-wrapper i{height:64px;line-height:64px}}div.app .moreinfo{font-size:100%}
+body{margin:0;color:#fff;background-color:#101010}html{font-family:"Arimo", "Arial", Sans-Serif}a{color:#99f;text-decoration:none}.visually-hidden{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}p,div.about,div.config,div.input-field,div.notes{max-width:94%;margin-left:auto;margin-right:auto}div.journey,div.nextstop{max-width:98%;margin-left:auto;margin-right:auto}p{text-align:justify}div.content{width:100%;margin:0}.copyright{margin-top:1em;color:#999;clear:both}.wagonorder{position:relative;width:100%;height:100ex}.wagonorder.exit-unknown .section{left:1em;width:2em}.wagonorder.exit-unknown .wagon{left:3em;min-width:6em}.wagonorder.exit-unknown .details{left:10em;right:0em}.wagonorder.exit-left .section{left:1em;width:2em;background-color:#222}.wagonorder.exit-left .wagon{left:3em;min-width:6em}.wagonorder.exit-left .details{left:10em;right:0em}.wagonorder.exit-right .section{right:1em;width:2em;background-color:#222}.wagonorder.exit-right .wagon{right:3em;min-width:6em}.wagonorder.exit-right .details{right:10em;left:0em;text-align:right}.wagonorder .section{position:absolute;text-align:center}.wagonorder .wagon{position:absolute;border:1px solid #999;padding-left:0.2em;padding-right:0.2em}.wagonorder .wagon .material-icons{color:#bbb}.wagonorder .wagon .direction{position:absolute;left:0.2em;bottom:0;right:0;text-align:center;color:#bbb}.wagonorder .wagon~.wagon{border-top:none}.wagonorder .firstclass{background-color:#330}.wagonorder .powercar{background-color:#222}.wagonorder .closed{background-color:#222}.wagonorder .nondestwagon{border-style:dashed}.wagonorder .details{position:absolute;padding-top:0.5ex}.wagonorder .details .type{display:inline-block;width:5em;color:#fff}.wagonorder .details a.type{color:#99f}.wagonorder .details .groupno{color:#fff}.wagonorder .details .grouptype{color:#bbb}.wagonorder .details .grouptype:before{content:"("}.wagonorder .details .grouptype:after{content:")"}.wagonorder .details .uicunknown{color:#999}.wagonorder .details .uicexchange{margin-right:0.2em;color:#999}.wagonorder .details .uiccountry{margin-right:0.2em;color:#999}.wagonorder .details .uic5{margin-right:0.2em;color:#999}.wagonorder .details .uic56{color:#bbb;font-weight:bold}.wagonorder .details .uic78{margin-right:0.2em;color:#bbb;font-weight:bold}.wagonorder .details .uic78:before{content:"-"}.wagonorder .details .uictype{margin-right:0.2em;color:#bbb;font-weight:bold}.wagonorder .details .uicno{color:#bbb}.wagonorder .details .uiccheck{color:#999}.wagonorder .details .uiccheck:before{content:"-"}.singlewagon .sign-left{float:left;padding-left:5%}.singlewagon .sign-right{float:right;padding-right:5%}.singlewagon .sign-center{text-align:center}.singlewagon .platform{text-align:center;background-color:#444;font-weight:bold;padding-top:0.5em;padding-bottom:0.5em}.singlewagon img.wagonfile{width:100%;margin-top:0.2em;margin-bottom:0.2em}div.app{border-width:1px 2px;width:100%;margin-bottom:5em}div.app>ul{position:relative;width:100%;list-style-type:none;margin:0;padding:0}div.app>ul>li{min-height:7em;display:block;width:100%;position:relative;border-bottom:1px solid #999;background-color:#101010}div.app>ul>li.cancelled{background-color:#512f00}div.app>ul>li.cancelled .time{color:#fff !important}div.app>ul>li.past{opacity:0.8;background-color:#222}div.app>ul>li>a{color:#fff}div.app>ul>li .anchor{position:relative;top:-12em}div.app>ul>li .line{font-size:2.7em;position:absolute;bottom:5px;left:2px;max-width:6em;max-height:3ex;overflow:hidden}div.app>ul>li .line .trainno{font-weight:normal}div.app>ul>li .line .trainno_sub{font-weight:normal;font-size:0.6em;text-align:center;margin-top:-0.2em}div.app>ul>li .sbahn .trainno_sub{font-weight:normal;font-size:0.5em;text-align:center;margin-top:-0.25em}div.app>ul>li .lineinfo{color:#fff;font-size:2em;position:absolute;top:0px;left:2px}div.app>ul>li .route,div.app>ul>li .info{background-color:transparent;font-size:2.1em;position:absolute;top:0;left:7.7em;right:7em;height:1.5em;overflow:hidden;white-space:nowrap}div.app>ul>li .route{color:#ddd}div.app>ul>li .info{color:#f77}div.app>ul>li .dest,div.app>ul>li .origin{background-color:transparent;font-size:4em;position:absolute;bottom:0;left:4em;width:70%;white-space:nowrap;overflow:hidden;color:#fff}div.app>ul>li .dest{background-color:transparent;color:#fff}div.app>ul>li .origin{background-color:transparent;color:#bbb}div.app>ul>li .origin:before{content:"von "}div.app>ul>li .load{color:#fff;font-weight:normal;margin-right:0.5em}div.app>ul>li .platform{background-color:transparent;font-size:3em;font-weight:bold;position:absolute;right:5px;bottom:0;padding-left:0.2em;color:#fff}div.app>ul>li .changed-platform{color:#f77}div.app>ul>li .time{background-color:transparent;font-size:2.3em;position:absolute;right:5px;top:1px;padding-left:0.2em;color:#fff}div.app>ul>li .time.delayed{color:#f77;background-color:transparent}div.app>ul>li .time.a-bit-delayed{color:#d99;background-color:transparent}div.app>ul>li .time.on-time{color:#aea;background-color:transparent}div.app>ul>li .time .no-realtime{background-color:transparent;padding-right:1ex}div.app>ul>li .time .no-realtime i.material-icons{font-size:12px}div.app>ul>li .time .delay{font-size:1em;color:#f77;background-color:transparent;padding-right:1ex}div.app>ul>li .time .undelay{font-size:1em;color:#7f7;padding-right:1ex}div.app>ul>li .time .delaynorm{font-size:0.9em;color:#d99}div.app>ul>li .time .undelaynorm{font-size:0.9em;color:#9d9}div.app .trainsubtype{font-weight:normal;font-size:70%;position:relative;vertical-align:baseline;top:-0.6ex;left:-0.5ex}div.app .replacement{color:#afa}div.app .replaced{color:#faa}div.app .sbahn{font-weight:bold;border-radius:30px;padding:3px 6px 2px 6px;background-color:#151}div.app .bahn,div.app .fern,div.app .ext{font-weight:bold;border-radius:5px;padding:3px 5px 2px 5px}div.app .bahn{background-color:#333}div.app .fern{background-color:#511}div.app .ext{border:2px solid #333}div.app .tram,div.app .bus,div.app .ubahn{padding:3px 5px 2px 5px}div.app .tram{background-color:#411}div.app .bus{background-color:#515}div.app .ubahn{background-color:#071e62}div.app .moreinfo{font-size:2.1em;position:fixed;left:0;right:0;bottom:0em;z-index:5;overflow:auto;cursor:default;background-color:#101010}div.app .moreinfo .mheader,div.app .moreinfo .mfooter{max-width:50em;margin-left:auto;margin-right:auto}div.app .moreinfo .mheader{text-align:center;font-size:120%;padding-top:0.5em;padding-bottom:0.5em;padding-left:1em;padding-right:1em;border-bottom:0.1em dashed #cccccc}div.app .moreinfo .mfooter{padding-top:0.5em;padding-left:1em;padding-right:1em}div.app .moreinfo .dataline{font-size:120%;width:100%;display:flex;justify-content:space-between;margin-bottom:0.5em}div.app .moreinfo .dataline>div{width:33%}div.app .moreinfo .wagonorder-preview{font-size:110%;width:100%;text-align:center;margin-bottom:1em}div.app .moreinfo .wagonorder-preview a{color:#fff}div.app .moreinfo .wagonorder-preview .otherno{color:#bbb}div.app .moreinfo .wagonorder-preview .meta{color:#ddd}div.app .moreinfo .departure{text-align:right}div.app .moreinfo .platform{text-align:center}div.app .moreinfo .arrival{display:inline-block;text-align:right}div.app .moreinfo .loading{text-align:center;width:100%;color:#888888}div.app .moreinfo .minfo{color:#f77}div.app .moreinfo .timehidden{color:#bbb}div.app .moreinfo .undelay{color:#7f7}div.app .moreinfo .verbose{margin-bottom:1em}div.app .moreinfo .verbose .no-realtime{color:#f77}div.app .moreinfo .messages i.material-icons{font-size:14px}div.app .moreinfo .details{margin-top:1em}div.app .moreinfo .mroute .important-stop{color:#fff}div.app .moreinfo .mroute .generic-stop{color:#bbb}div.app .moreinfo .mroute .additional-stop{color:#7f7}div.app .moreinfo .mroute .cancelled-stop{color:#f77}div.app .moreinfo .mroute .past-stop{list-style-type:disc}div.app .moreinfo .mroute .future-stop{list-style-type:circle}div.app .moreinfo .mroute .time-early{color:#cfc}div.app .moreinfo .mroute .time-delayed{color:#f99}div.app .moreinfo .mroute .time-sched-only{color:#f99}div.app .moreinfo .mroute .time-sched-ontime{color:#cfc}div.app .moreinfo .mroute .annotation{color:#bbb;list-style-type:none;padding-left:3em}div.app .moreinfo .mroute .-sched:before{content:" "}div.app .moreinfo .mroute .time-sched:after{content:" "}div.app .moreinfo .mroute .time-sched-only:before{content:"("}div.app .moreinfo .mroute .time-sched-only:after{content:")"}div.app .moreinfo .mroute i.material-icons{font-size:14px}div.app .moreinfo .db-attr{margin-bottom:1em}div.app .moreinfo .db-attr span{margin-right:0.5em}div.app .collapsed-moreinfo{display:none}div.app .expanded-moreinfo{display:block}ul.ui-autocomplete{max-height:20em;overflow-x:hidden;overflow-y:auto}div.geolocation{text-align:center}div.candidatestatus{text-align:center;color:#999999}div.candidatelist a{display:block;text-decoration:none;font-size:1.4em;padding-top:0.3em;text-align:center;border-bottom:1px solid #999999}div.candidatelist a .distance:after{content:" km"}div.candidatelist a .distance{font-size:0.6em;color:#999999;padding-top:0.2em;padding-bottom:0.3em}div.candidatelist a .traininfo{font-size:0.7em;color:#999999;padding-top:0.2em;padding-bottom:0.3em}div.config{margin-top:2em;font-family:Sans-Serif;color:#bbb}div.config a{color:#99f;cursor:pointer;text-decoration:none}div.about{margin-top:1em;font-family:Sans-Serif;color:#bbb}div.about a{color:#99f;text-decoration:none}.notice{padding:15px;margin-bottom:20px;border:1px solid #bce8f1;border-radius:4px;color:#31708f;background-color:#d9edf7;margin-left:auto;margin-right:auto}.warning{padding:15px;margin-bottom:20px;border:1px solid #faebcc;border-radius:4px;color:#8a6d3b;background-color:#fcf8e3;margin-left:auto;margin-right:auto}.error{padding:15px;margin-bottom:20px;border:1px solid #ebccd1;border-radius:4px;color:#a94442;background-color:#f2dede;margin-left:auto;margin-right:auto}.error .errcode{font-family:Monospace;margin-top:2em;font-size:100%;color:#aaaaaa}.container{max-width:60em;margin-left:auto;margin-right:auto}pre{margin-bottom:2em}span.optional,span.notes{color:#bbb}.moresettings-header{cursor:pointer}.moresettings-header-collapsed:before{content:"▹ "}.moresettings-header-expanded:before{content:"▿ "}.moresettings-collapsed{display:none}.moresettings-expanded{display:block}.developers-header{cursor:pointer}.developers-header-collapsed:before{content:"▹ "}.developers-header-expanded:before{content:"▿ "}.developers-collapsed{display:none}.developers-expanded{display:block}div.break{height:1em}div.field{margin-top:0.3em;margin-bottom:0.6em}.disabledbutton{display:inline-block;vertical-align:baseline;border-radius:4px;border:1px solid #cccccc;box-shadow:none;padding:0.9ex;margin-right:1em}.smallbutton{display:inline-block;vertical-align:baseline;border-radius:4px;border:1px solid #2e6da4;transition:background-color .3s;color:#fff;background-color:#337ab7;cursor:pointer;box-shadow:none;padding:0.9ex;margin-right:1em}.smallbutton .material-icons,.disabledbutton .material-icons{display:block;float:left;margin-right:0.5ex}.smallbutton img{display:block;float:left;margin-right:0.7ex;height:1.2em}input,select,.button{display:inline-block;width:60em;max-width:100%;min-height:1.8em;border-radius:4px;color:#fff;background-color:#101010;border:1px solid #444;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);font-size:90%;text-align:center;vertical-align:middle}input[type="text"]{width:59em;padding-left:0.5em;padding-right:0.5em;text-align:left;box-sizing:border-box}select{min-height:2em}input[type="checkbox"]{width:1.5em;box-shadow:none}input[type="submit"],.button{transition:background-color .3s;color:#fff;background-color:#337ab7;border-color:#2e6da4;cursor:pointer;box-shadow:none;padding-top:0.9ex;padding-bottom:0.9ex}.button{padding-top:1.1ex;padding-bottom:0}input[type="submit"]:active,input[type="submit"]:focus,input[type="submit"]:hover,.button:active,.button:focus,.button:hover,.smallbutton:active,.smallbutton:focus,.smallbutton:hover{color:#fff;background-color:#286090;border-color:#204d74}input[type="submit"]:active,.button:active{box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.button-active{font-weight:bold}.button-light{color:#ddd;background-color:#101010;border-color:#444}.button-light:active,.button-light:focus,.button-light:hover{color:#ddd;background-color:#111;border-color:#333}div.backendlink{margin-top:1ex}div.notes{margin-top:2em}div.notes ul{margin-top:1em}div.app{max-width:60em;margin-left:auto;margin-right:auto}.navbar-fixed{position:relative;z-index:997}.navbar-fixed nav{position:fixed}nav{box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}nav{width:100%;overflow:hidden}nav a{color:#fff}nav .nav-wrapper{position:relative;height:100%}nav i,nav i.material-icons{display:block;font-size:24px}nav .brand-logo{position:absolute;display:inline-block;padding-left:0.5rem}nav ul{margin:0;padding-left:0;list-style-type:none}nav ul li{transition:background-color .3s;float:left;padding:0;list-style-type:none;background-color:#00838f}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}@media only screen and (max-width: 600px){div.app>ul>li{font-size:35%}div.navbar-fixed{height:56px}.moreinfo{top:56px}nav{height:56px;line-height:56px}nav .brand-logo{font-size:1.5rem}nav .nav-wrapper i{height:56px;line-height:56px}}@media only screen and (min-width: 600px){div.app>ul>li{font-size:40%}div.navbar-fixed{height:64px}.moreinfo{top:64px}nav{height:64px;line-height:64px}nav .brand-logo{font-size:2.1rem}nav .nav-wrapper i{height:64px;line-height:64px}}div.app .moreinfo{font-size:100%}
diff --git a/public/static/css/mobile.css b/public/static/css/legacy-mobile.css
index 0bf84d4..0bf84d4 100644
--- a/public/static/css/mobile.css
+++ b/public/static/css/legacy-mobile.css
diff --git a/public/static/css/default.css b/public/static/css/legacy.css
index ac2eb79..ac2eb79 100644
--- a/public/static/css/default.css
+++ b/public/static/css/legacy.css
diff --git a/public/static/css/light.min.css b/public/static/css/light.min.css
index 14cda18..3128641 100644
--- a/public/static/css/light.min.css
+++ b/public/static/css/light.min.css
@@ -1 +1 @@
-body{margin:0;color:#000;background-color:#fff}html{font-family:"Arimo", "Arial", Sans-Serif}a{color:#009;text-decoration:none}.visually-hidden{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}p,div.about,div.input-field,div.notes{max-width:94%;margin-left:auto;margin-right:auto}p{text-align:justify}div.content{width:100%;margin:0}.copyright{margin-top:1em;color:#999;clear:both}.wagonorder{position:relative;width:100%;height:100ex}.wagonorder.exit-unknown .section{left:1em;width:2em}.wagonorder.exit-unknown .wagon{left:3em;min-width:6em}.wagonorder.exit-unknown .details{left:10em;right:0em}.wagonorder.exit-left .section{left:1em;width:2em;background-color:#ddd}.wagonorder.exit-left .wagon{left:3em;min-width:6em}.wagonorder.exit-left .details{left:10em;right:0em}.wagonorder.exit-right .section{right:1em;width:2em;background-color:#ddd}.wagonorder.exit-right .wagon{right:3em;min-width:6em}.wagonorder.exit-right .details{right:10em;left:0em;text-align:right}.wagonorder .section{position:absolute;text-align:center}.wagonorder .wagon{position:absolute;border:1px solid #999;padding-left:0.2em;padding-right:0.2em}.wagonorder .wagon .material-icons{color:#666}.wagonorder .wagon .direction{position:absolute;left:0.2em;bottom:0;right:0;text-align:center;color:#666}.wagonorder .wagon~.wagon{border-top:none}.wagonorder .firstclass{background-color:#ff9}.wagonorder .powercar{background-color:#ccc}.wagonorder .nondestwagon{border-style:dashed}.wagonorder .details{position:absolute;padding-top:0.5ex}.wagonorder .details .type{display:inline-block;width:5em;color:#666}.wagonorder .details a.type{color:#009}.wagonorder .details .uicunknown{color:#999}.wagonorder .details .uicexchange{margin-right:0.2em;color:#999}.wagonorder .details .uiccountry{margin-right:0.2em;color:#999}.wagonorder .details .uic5{margin-right:0.2em;color:#999}.wagonorder .details .uic56{color:#666;font-weight:bold}.wagonorder .details .uic78{margin-right:0.2em;color:#666;font-weight:bold}.wagonorder .details .uic78::before{content:"-"}.wagonorder .details .uictype{margin-right:0.2em;color:#666;font-weight:bold}.wagonorder .details .uicno{color:#666}.wagonorder .details .uiccheck{color:#999}.wagonorder .details .uiccheck::before{content:"-"}.singlewagon .sign-left{float:left;padding-left:5%}.singlewagon .sign-right{float:right;padding-right:5%}.singlewagon .sign-center{text-align:center}.singlewagon .platform{text-align:center;background-color:#ccc;font-weight:bold;padding-top:0.5em;padding-bottom:0.5em}.singlewagon img.wagonfile{width:100%;margin-top:0.2em;margin-bottom:0.2em}div.app{border-width:1px 2px;width:100%;margin-bottom:5em}div.app>ul{position:relative;width:100%;list-style-type:none;margin:0;padding:0}div.app>ul>li{min-height:7em;display:block;width:100%;position:relative;border-bottom:1px solid #999;background-color:#fff}div.app>ul>li.cancelled{background-color:#ffe7d0}div.app>ul>li.past{opacity:0.8;background-color:#ddd}div.app>ul>li>a{color:#000}div.app>ul>li .anchor{position:relative;top:-12em}div.app>ul>li .line{font-size:2.7em;position:absolute;bottom:5px;left:2px;max-width:6em;max-height:3ex;overflow:hidden}div.app>ul>li .line .trainno{font-weight:normal}div.app>ul>li .line .trainno_sub{font-weight:normal;font-size:0.6em;text-align:center;margin-top:-0.2em}div.app>ul>li .sbahn .trainno_sub{font-weight:normal;font-size:0.5em;text-align:center;margin-top:-0.25em}div.app>ul>li .lineinfo{color:#000;font-size:2em;position:absolute;top:0px;left:2px}div.app>ul>li .route,div.app>ul>li .info{background-color:transparent;font-size:2.1em;position:absolute;top:0;left:7.7em;right:7em;height:1.5em;overflow:hidden}div.app>ul>li .route{color:#444}div.app>ul>li .info{color:red}div.app>ul>li .dest,div.app>ul>li .origin{background-color:transparent;font-size:4em;position:absolute;bottom:0;left:4em;width:70%;overflow:hidden;color:#000}div.app>ul>li .dest{background-color:transparent;color:#000}div.app>ul>li .origin{background-color:transparent;color:#666}div.app>ul>li .origin:before{content:"von "}div.app>ul>li .platform{background-color:transparent;font-size:3em;font-weight:bold;position:absolute;right:5px;bottom:0;padding-left:0.2em;color:#000}div.app>ul>li .changed-platform{color:red}div.app>ul>li .time{background-color:transparent;font-size:2.3em;position:absolute;right:5px;top:1px;padding-left:0.2em;color:#000}div.app>ul>li .time.delayed{color:red;background-color:transparent}div.app>ul>li .time .no-realtime{background-color:transparent;padding-right:1ex}div.app>ul>li .time .no-realtime i.material-icons{font-size:12px}div.app>ul>li .time .delay{font-size:1em;color:red;background-color:transparent;padding-right:1ex}div.app>ul>li .time .undelay{font-size:1em;color:#060;padding-right:1ex}div.app>ul>li .time .delaynorm{font-size:0.9em;color:#b33}div.app>ul>li .time .undelaynorm{font-size:0.9em;color:#383}div.app .trainsubtype{font-weight:normal;font-size:70%;position:relative;vertical-align:baseline;top:-0.6ex;left:-0.5ex}div.app .replacement{color:#060}div.app .replaced{color:#600}div.app .sbahn{font-weight:bold;border-radius:30px;padding:3px 6px 2px 6px;background-color:#95d79f}div.app .bahn,div.app .fern,div.app .ext{font-weight:bold;border-radius:5px;padding:3px 5px 2px 5px}div.app .bahn{background-color:#eee}div.app .fern{background-color:#fdd}div.app .ext{border:2px solid #eee}div.app .tram,div.app .bus,div.app .ubahn{padding:3px 5px 2px 5px}div.app .tram{background-color:#fcc}div.app .bus{background-color:#eae}div.app .ubahn{background-color:#aac0ff}div.app .moreinfo{font-size:2.1em;position:fixed;left:0;right:0;bottom:0em;z-index:5;overflow:auto;cursor:default;background-color:#fff}div.app .moreinfo .mheader,div.app .moreinfo .mfooter{max-width:50em;margin-left:auto;margin-right:auto}div.app .moreinfo .mheader{text-align:center;font-size:120%;padding-top:0.5em;padding-bottom:0.5em;padding-left:1em;padding-right:1em;border-bottom:0.1em dashed #cccccc}div.app .moreinfo .mfooter{padding-top:0.5em;padding-left:1em;padding-right:1em}div.app .moreinfo .dataline{font-size:120%;width:100%;display:flex;justify-content:space-between;margin-bottom:1em}div.app .moreinfo .dataline>div{width:33%}div.app .moreinfo .departure{text-align:right}div.app .moreinfo .platform{text-align:center}div.app .moreinfo .arrival{display:inline-block;text-align:right}div.app .moreinfo .loading{text-align:center;width:100%;color:#888888}div.app .moreinfo .minfo{color:red}div.app .moreinfo .timehidden{color:#666}div.app .moreinfo .undelay{color:#060}div.app .moreinfo .verbose{margin-bottom:1em}div.app .moreinfo .verbose .no-realtime{color:#c00}div.app .moreinfo .messages i.material-icons{font-size:14px}div.app .moreinfo .details{margin-top:1em}div.app .moreinfo .mroute .important-stop{color:#000}div.app .moreinfo .mroute .generic-stop{color:#666}div.app .moreinfo .mroute .additional-stop{color:#090}div.app .moreinfo .mroute .cancelled-stop{color:#c00}div.app .moreinfo .mroute .past-stop{list-style-type:disc}div.app .moreinfo .mroute .future-stop{list-style-type:circle}div.app .moreinfo .mroute i.material-icons{font-size:14px}div.app .moreinfo .db-attr{margin-bottom:1em}div.app .moreinfo .db-attr span{margin-right:0.5em}div.app .collapsed-moreinfo{display:none}div.app .expanded-moreinfo{display:block}ul.ui-autocomplete{max-height:20em;overflow-x:hidden;overflow-y:auto}div.geolocation{text-align:center}div.candidatestatus{text-align:center;color:#999999}div.candidatelist a{display:block;text-decoration:none;font-size:1.4em;padding-top:0.3em;text-align:center;border-bottom:1px solid #999999}div.candidatelist a .distance:after{content:" km"}div.candidatelist a .distance{font-size:0.6em;color:#999999;padding-top:0.2em;padding-bottom:0.3em}div.candidatelist a .traininfo{font-size:0.7em;color:#999999;padding-top:0.2em;padding-bottom:0.3em}div.about{margin-top:2em;font-family:Sans-Serif;color:#666}div.about a{color:#009;text-decoration:none}.notice{padding:15px;margin-bottom:20px;border:1px solid #bce8f1;border-radius:4px;color:#31708f;background-color:#d9edf7;margin-left:auto;margin-right:auto}.warning{padding:15px;margin-bottom:20px;border:1px solid #faebcc;border-radius:4px;color:#8a6d3b;background-color:#fcf8e3;margin-left:auto;margin-right:auto}.error{padding:15px;margin-bottom:20px;border:1px solid #ebccd1;border-radius:4px;color:#a94442;background-color:#f2dede;margin-left:auto;margin-right:auto}.error .errcode{font-family:Monospace;margin-top:2em;font-size:100%;color:#aaaaaa}.container{max-width:60em;margin-left:auto;margin-right:auto}pre{margin-bottom:2em}span.optional,span.notes{color:#666}.moresettings-header{cursor:pointer}.moresettings-header-collapsed:before{content:"▹ "}.moresettings-header-expanded:before{content:"▿ "}.moresettings-collapsed{display:none}.moresettings-expanded{display:block}.developers-header{cursor:pointer}.developers-header-collapsed:before{content:"▹ "}.developers-header-expanded:before{content:"▿ "}.developers-collapsed{display:none}.developers-expanded{display:block}div.break{height:1em}div.field{margin-top:0.3em;margin-bottom:0.6em}.disabledbutton{display:inline-block;vertical-align:baseline;border-radius:4px;border:1px solid #cccccc;box-shadow:none;padding:0.9ex;margin-right:1em}.smallbutton{display:inline-block;vertical-align:baseline;border-radius:4px;border:1px solid #2e6da4;transition:background-color .3s;color:#fff;background-color:#337ab7;cursor:pointer;box-shadow:none;padding:0.9ex;margin-right:1em}.smallbutton .material-icons,.disabledbutton .material-icons{display:block;float:left;margin-right:0.5ex}.smallbutton img{display:block;float:left;margin-right:0.7ex;height:1.2em}input,select,.button{display:inline-block;width:60em;max-width:100%;min-height:1.8em;border-radius:4px;color:#000;background-color:#fff;border:1px solid #ccc;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);font-size:90%;text-align:center;vertical-align:middle}input[type="text"]{width:59em;padding-left:0.5em;padding-right:0.5em;text-align:left;box-sizing:border-box}select{min-height:2em}input[type="checkbox"]{width:1.5em;box-shadow:none}input[type="submit"],.button{transition:background-color .3s;color:#fff;background-color:#337ab7;border-color:#2e6da4;cursor:pointer;box-shadow:none;padding-top:0.9ex;padding-bottom:0.9ex}.button{padding-top:1.1ex;padding-bottom:0}input[type="submit"]:active,input[type="submit"]:focus,input[type="submit"]:hover,.button:active,.button:focus,.button:hover,.smallbutton:active,.smallbutton:focus,.smallbutton:hover{color:#fff;background-color:#286090;border-color:#204d74}input[type="submit"]:active,.button:active{box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.button-light{color:#333;background-color:#fff;border-color:#ccc}.button-light:active,.button-light:focus,.button-light:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}div.notes{margin-top:2em}div.notes ul{margin-top:1em}div.app{max-width:60em;margin-left:auto;margin-right:auto}.navbar-fixed{position:relative;z-index:997}.navbar-fixed nav{position:fixed}nav{box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}nav{width:100%;overflow:hidden}nav a{color:#fff}nav .nav-wrapper{position:relative;height:100%}nav i,nav i.material-icons{display:block;font-size:24px}nav .brand-logo{position:absolute;display:inline-block;padding-left:0.5rem}nav ul{margin:0;padding-left:0;list-style-type:none}nav ul li{transition:background-color .3s;float:left;padding:0;list-style-type:none;background-color:#00838f}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}@media only screen and (max-width: 600px){div.app>ul>li{font-size:35%}div.navbar-fixed{height:56px}.moreinfo{top:56px}nav{height:56px;line-height:56px}nav .brand-logo{font-size:1.5rem}nav .nav-wrapper i{height:56px;line-height:56px}}@media only screen and (min-width: 600px){div.app>ul>li{font-size:40%}div.navbar-fixed{height:64px}.moreinfo{top:64px}nav{height:64px;line-height:64px}nav .brand-logo{font-size:2.1rem}nav .nav-wrapper i{height:64px;line-height:64px}}div.app .moreinfo{font-size:100%}
+body{margin:0;color:#000;background-color:#fff}html{font-family:"Arimo", "Arial", Sans-Serif}a{color:#009;text-decoration:none}.visually-hidden{clip:rect(0 0 0 0);clip-path:inset(50%);height:1px;overflow:hidden;position:absolute;white-space:nowrap;width:1px}p,div.about,div.config,div.input-field,div.notes{max-width:94%;margin-left:auto;margin-right:auto}div.journey,div.nextstop{max-width:98%;margin-left:auto;margin-right:auto}p{text-align:justify}div.content{width:100%;margin:0}.copyright{margin-top:1em;color:#999;clear:both}.wagonorder{position:relative;width:100%;height:100ex}.wagonorder.exit-unknown .section{left:1em;width:2em}.wagonorder.exit-unknown .wagon{left:3em;min-width:6em}.wagonorder.exit-unknown .details{left:10em;right:0em}.wagonorder.exit-left .section{left:1em;width:2em;background-color:#ddd}.wagonorder.exit-left .wagon{left:3em;min-width:6em}.wagonorder.exit-left .details{left:10em;right:0em}.wagonorder.exit-right .section{right:1em;width:2em;background-color:#ddd}.wagonorder.exit-right .wagon{right:3em;min-width:6em}.wagonorder.exit-right .details{right:10em;left:0em;text-align:right}.wagonorder .section{position:absolute;text-align:center}.wagonorder .wagon{position:absolute;border:1px solid #999;padding-left:0.2em;padding-right:0.2em}.wagonorder .wagon .material-icons{color:#666}.wagonorder .wagon .direction{position:absolute;left:0.2em;bottom:0;right:0;text-align:center;color:#666}.wagonorder .wagon~.wagon{border-top:none}.wagonorder .firstclass{background-color:#ff9}.wagonorder .powercar{background-color:#ccc}.wagonorder .closed{background-color:#ddd}.wagonorder .nondestwagon{border-style:dashed}.wagonorder .details{position:absolute;padding-top:0.5ex}.wagonorder .details .type{display:inline-block;width:5em;color:#000}.wagonorder .details a.type{color:#009}.wagonorder .details .groupno{color:#000}.wagonorder .details .grouptype{color:#666}.wagonorder .details .grouptype:before{content:"("}.wagonorder .details .grouptype:after{content:")"}.wagonorder .details .uicunknown{color:#999}.wagonorder .details .uicexchange{margin-right:0.2em;color:#999}.wagonorder .details .uiccountry{margin-right:0.2em;color:#999}.wagonorder .details .uic5{margin-right:0.2em;color:#999}.wagonorder .details .uic56{color:#666;font-weight:bold}.wagonorder .details .uic78{margin-right:0.2em;color:#666;font-weight:bold}.wagonorder .details .uic78:before{content:"-"}.wagonorder .details .uictype{margin-right:0.2em;color:#666;font-weight:bold}.wagonorder .details .uicno{color:#666}.wagonorder .details .uiccheck{color:#999}.wagonorder .details .uiccheck:before{content:"-"}.singlewagon .sign-left{float:left;padding-left:5%}.singlewagon .sign-right{float:right;padding-right:5%}.singlewagon .sign-center{text-align:center}.singlewagon .platform{text-align:center;background-color:#ccc;font-weight:bold;padding-top:0.5em;padding-bottom:0.5em}.singlewagon img.wagonfile{width:100%;margin-top:0.2em;margin-bottom:0.2em}div.app{border-width:1px 2px;width:100%;margin-bottom:5em}div.app>ul{position:relative;width:100%;list-style-type:none;margin:0;padding:0}div.app>ul>li{min-height:7em;display:block;width:100%;position:relative;border-bottom:1px solid #999;background-color:#fff}div.app>ul>li.cancelled{background-color:#ffe7d0}div.app>ul>li.cancelled .time{color:#000 !important}div.app>ul>li.past{opacity:0.8;background-color:#ddd}div.app>ul>li>a{color:#000}div.app>ul>li .anchor{position:relative;top:-12em}div.app>ul>li .line{font-size:2.7em;position:absolute;bottom:5px;left:2px;max-width:6em;max-height:3ex;overflow:hidden}div.app>ul>li .line .trainno{font-weight:normal}div.app>ul>li .line .trainno_sub{font-weight:normal;font-size:0.6em;text-align:center;margin-top:-0.2em}div.app>ul>li .sbahn .trainno_sub{font-weight:normal;font-size:0.5em;text-align:center;margin-top:-0.25em}div.app>ul>li .lineinfo{color:#000;font-size:2em;position:absolute;top:0px;left:2px}div.app>ul>li .route,div.app>ul>li .info{background-color:transparent;font-size:2.1em;position:absolute;top:0;left:7.7em;right:7em;height:1.5em;overflow:hidden;white-space:nowrap}div.app>ul>li .route{color:#444}div.app>ul>li .info{color:red}div.app>ul>li .dest,div.app>ul>li .origin{background-color:transparent;font-size:4em;position:absolute;bottom:0;left:4em;width:70%;white-space:nowrap;overflow:hidden;color:#000}div.app>ul>li .dest{background-color:transparent;color:#000}div.app>ul>li .origin{background-color:transparent;color:#666}div.app>ul>li .origin:before{content:"von "}div.app>ul>li .load{color:#000;font-weight:normal;margin-right:0.5em}div.app>ul>li .platform{background-color:transparent;font-size:3em;font-weight:bold;position:absolute;right:5px;bottom:0;padding-left:0.2em;color:#000}div.app>ul>li .changed-platform{color:red}div.app>ul>li .time{background-color:transparent;font-size:2.3em;position:absolute;right:5px;top:1px;padding-left:0.2em;color:#000}div.app>ul>li .time.delayed{color:red;background-color:transparent}div.app>ul>li .time.a-bit-delayed{color:#b33;background-color:transparent}div.app>ul>li .time.on-time{color:#272;background-color:transparent}div.app>ul>li .time .no-realtime{background-color:transparent;padding-right:1ex}div.app>ul>li .time .no-realtime i.material-icons{font-size:12px}div.app>ul>li .time .delay{font-size:1em;color:red;background-color:transparent;padding-right:1ex}div.app>ul>li .time .undelay{font-size:1em;color:#060;padding-right:1ex}div.app>ul>li .time .delaynorm{font-size:0.9em;color:#b33}div.app>ul>li .time .undelaynorm{font-size:0.9em;color:#383}div.app .trainsubtype{font-weight:normal;font-size:70%;position:relative;vertical-align:baseline;top:-0.6ex;left:-0.5ex}div.app .replacement{color:#060}div.app .replaced{color:#600}div.app .sbahn{font-weight:bold;border-radius:30px;padding:3px 6px 2px 6px;background-color:#95d79f}div.app .bahn,div.app .fern,div.app .ext{font-weight:bold;border-radius:5px;padding:3px 5px 2px 5px}div.app .bahn{background-color:#eee}div.app .fern{background-color:#fdd}div.app .ext{border:2px solid #eee}div.app .tram,div.app .bus,div.app .ubahn{padding:3px 5px 2px 5px}div.app .tram{background-color:#fcc}div.app .bus{background-color:#eae}div.app .ubahn{background-color:#aac0ff}div.app .moreinfo{font-size:2.1em;position:fixed;left:0;right:0;bottom:0em;z-index:5;overflow:auto;cursor:default;background-color:#fff}div.app .moreinfo .mheader,div.app .moreinfo .mfooter{max-width:50em;margin-left:auto;margin-right:auto}div.app .moreinfo .mheader{text-align:center;font-size:120%;padding-top:0.5em;padding-bottom:0.5em;padding-left:1em;padding-right:1em;border-bottom:0.1em dashed #cccccc}div.app .moreinfo .mfooter{padding-top:0.5em;padding-left:1em;padding-right:1em}div.app .moreinfo .dataline{font-size:120%;width:100%;display:flex;justify-content:space-between;margin-bottom:0.5em}div.app .moreinfo .dataline>div{width:33%}div.app .moreinfo .wagonorder-preview{font-size:110%;width:100%;text-align:center;margin-bottom:1em}div.app .moreinfo .wagonorder-preview a{color:#000}div.app .moreinfo .wagonorder-preview .otherno{color:#666}div.app .moreinfo .wagonorder-preview .meta{color:#333}div.app .moreinfo .departure{text-align:right}div.app .moreinfo .platform{text-align:center}div.app .moreinfo .arrival{display:inline-block;text-align:right}div.app .moreinfo .loading{text-align:center;width:100%;color:#888888}div.app .moreinfo .minfo{color:red}div.app .moreinfo .timehidden{color:#666}div.app .moreinfo .undelay{color:#060}div.app .moreinfo .verbose{margin-bottom:1em}div.app .moreinfo .verbose .no-realtime{color:#c00}div.app .moreinfo .messages i.material-icons{font-size:14px}div.app .moreinfo .details{margin-top:1em}div.app .moreinfo .mroute .important-stop{color:#000}div.app .moreinfo .mroute .generic-stop{color:#666}div.app .moreinfo .mroute .additional-stop{color:#090}div.app .moreinfo .mroute .cancelled-stop{color:#c00}div.app .moreinfo .mroute .past-stop{list-style-type:disc}div.app .moreinfo .mroute .future-stop{list-style-type:circle}div.app .moreinfo .mroute .time-early{color:#070}div.app .moreinfo .mroute .time-delayed{color:#900}div.app .moreinfo .mroute .time-sched-only{color:#900}div.app .moreinfo .mroute .time-sched-ontime{color:#070}div.app .moreinfo .mroute .annotation{color:#666;list-style-type:none;padding-left:3em}div.app .moreinfo .mroute .-sched:before{content:" "}div.app .moreinfo .mroute .time-sched:after{content:" "}div.app .moreinfo .mroute .time-sched-only:before{content:"("}div.app .moreinfo .mroute .time-sched-only:after{content:")"}div.app .moreinfo .mroute i.material-icons{font-size:14px}div.app .moreinfo .db-attr{margin-bottom:1em}div.app .moreinfo .db-attr span{margin-right:0.5em}div.app .collapsed-moreinfo{display:none}div.app .expanded-moreinfo{display:block}ul.ui-autocomplete{max-height:20em;overflow-x:hidden;overflow-y:auto}div.geolocation{text-align:center}div.candidatestatus{text-align:center;color:#999999}div.candidatelist a{display:block;text-decoration:none;font-size:1.4em;padding-top:0.3em;text-align:center;border-bottom:1px solid #999999}div.candidatelist a .distance:after{content:" km"}div.candidatelist a .distance{font-size:0.6em;color:#999999;padding-top:0.2em;padding-bottom:0.3em}div.candidatelist a .traininfo{font-size:0.7em;color:#999999;padding-top:0.2em;padding-bottom:0.3em}div.config{margin-top:2em;font-family:Sans-Serif;color:#666}div.config a{color:#009;cursor:pointer;text-decoration:none}div.about{margin-top:1em;font-family:Sans-Serif;color:#666}div.about a{color:#009;text-decoration:none}.notice{padding:15px;margin-bottom:20px;border:1px solid #bce8f1;border-radius:4px;color:#31708f;background-color:#d9edf7;margin-left:auto;margin-right:auto}.warning{padding:15px;margin-bottom:20px;border:1px solid #faebcc;border-radius:4px;color:#8a6d3b;background-color:#fcf8e3;margin-left:auto;margin-right:auto}.error{padding:15px;margin-bottom:20px;border:1px solid #ebccd1;border-radius:4px;color:#a94442;background-color:#f2dede;margin-left:auto;margin-right:auto}.error .errcode{font-family:Monospace;margin-top:2em;font-size:100%;color:#aaaaaa}.container{max-width:60em;margin-left:auto;margin-right:auto}pre{margin-bottom:2em}span.optional,span.notes{color:#666}.moresettings-header{cursor:pointer}.moresettings-header-collapsed:before{content:"▹ "}.moresettings-header-expanded:before{content:"▿ "}.moresettings-collapsed{display:none}.moresettings-expanded{display:block}.developers-header{cursor:pointer}.developers-header-collapsed:before{content:"▹ "}.developers-header-expanded:before{content:"▿ "}.developers-collapsed{display:none}.developers-expanded{display:block}div.break{height:1em}div.field{margin-top:0.3em;margin-bottom:0.6em}.disabledbutton{display:inline-block;vertical-align:baseline;border-radius:4px;border:1px solid #cccccc;box-shadow:none;padding:0.9ex;margin-right:1em}.smallbutton{display:inline-block;vertical-align:baseline;border-radius:4px;border:1px solid #2e6da4;transition:background-color .3s;color:#fff;background-color:#337ab7;cursor:pointer;box-shadow:none;padding:0.9ex;margin-right:1em}.smallbutton .material-icons,.disabledbutton .material-icons{display:block;float:left;margin-right:0.5ex}.smallbutton img{display:block;float:left;margin-right:0.7ex;height:1.2em}input,select,.button{display:inline-block;width:60em;max-width:100%;min-height:1.8em;border-radius:4px;color:#000;background-color:#fff;border:1px solid #ccc;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);font-size:90%;text-align:center;vertical-align:middle}input[type="text"]{width:59em;padding-left:0.5em;padding-right:0.5em;text-align:left;box-sizing:border-box}select{min-height:2em}input[type="checkbox"]{width:1.5em;box-shadow:none}input[type="submit"],.button{transition:background-color .3s;color:#fff;background-color:#337ab7;border-color:#2e6da4;cursor:pointer;box-shadow:none;padding-top:0.9ex;padding-bottom:0.9ex}.button{padding-top:1.1ex;padding-bottom:0}input[type="submit"]:active,input[type="submit"]:focus,input[type="submit"]:hover,.button:active,.button:focus,.button:hover,.smallbutton:active,.smallbutton:focus,.smallbutton:hover{color:#fff;background-color:#286090;border-color:#204d74}input[type="submit"]:active,.button:active{box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.button-active{font-weight:bold}.button-light{color:#333;background-color:#fff;border-color:#ccc}.button-light:active,.button-light:focus,.button-light:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}div.backendlink{margin-top:1ex}div.notes{margin-top:2em}div.notes ul{margin-top:1em}div.app{max-width:60em;margin-left:auto;margin-right:auto}.navbar-fixed{position:relative;z-index:997}.navbar-fixed nav{position:fixed}nav{box-shadow:0 2px 2px 0 rgba(0,0,0,0.14),0 3px 1px -2px rgba(0,0,0,0.12),0 1px 5px 0 rgba(0,0,0,0.2)}nav{width:100%;overflow:hidden}nav a{color:#fff}nav .nav-wrapper{position:relative;height:100%}nav i,nav i.material-icons{display:block;font-size:24px}nav .brand-logo{position:absolute;display:inline-block;padding-left:0.5rem}nav ul{margin:0;padding-left:0;list-style-type:none}nav ul li{transition:background-color .3s;float:left;padding:0;list-style-type:none;background-color:#00838f}nav ul a{transition:background-color .3s;font-size:1rem;color:#fff;display:block;padding:0 15px;cursor:pointer}@media only screen and (max-width: 600px){div.app>ul>li{font-size:35%}div.navbar-fixed{height:56px}.moreinfo{top:56px}nav{height:56px;line-height:56px}nav .brand-logo{font-size:1.5rem}nav .nav-wrapper i{height:56px;line-height:56px}}@media only screen and (min-width: 600px){div.app>ul>li{font-size:40%}div.navbar-fixed{height:64px}.moreinfo{top:64px}nav{height:64px;line-height:64px}nav .brand-logo{font-size:2.1rem}nav .nav-wrapper i{height:64px;line-height:64px}}div.app .moreinfo{font-size:100%}
diff --git a/public/static/css/material-icons.css b/public/static/css/material-icons.css
index 3706d9b..662e6b7 100644
--- a/public/static/css/material-icons.css
+++ b/public/static/css/material-icons.css
@@ -2,12 +2,12 @@
font-family: 'Material Icons';
font-style: normal;
font-weight: 400;
- src: url(/static/v85/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
+ src: url(/static/v110/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
- url(/static/v85/fonts/MaterialIcons-Regular.woff2) format('woff2'),
- url(/static/v85/fonts/MaterialIcons-Regular.woff) format('woff'),
- url(/static/v85/fonts/MaterialIcons-Regular.ttf) format('truetype');
+ url(/static/v110/fonts/MaterialIcons-Regular.woff2) format('woff2'),
+ url(/static/v110/fonts/MaterialIcons-Regular.woff) format('woff'),
+ url(/static/v110/fonts/MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
diff --git a/public/static/js/collapse.js b/public/static/js/collapse.js
index 2beead1..e861169 100644
--- a/public/static/js/collapse.js
+++ b/public/static/js/collapse.js
@@ -1,9 +1,22 @@
/*
- * Copyright (C) 2020 Birte Kristina Friesel
+ * Copyright (C) 2020-2023 Birte Kristina Friesel
*
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
+function setLang(lang) {
+ document.cookie = 'lang=' + lang + ';SameSite=None;Secure';
+ location.reload();
+}
+
+function setTheme(theme) {
+ localStorage.setItem('theme', theme);
+ if (!otherTheme.hasOwnProperty(theme)) {
+ theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
+ }
+ addStyleSheet(theme, 'theme');
+}
+
function reload_app() {
// TODO use a variable instead of window.location.href, as
// window.location.href may be /z/...
@@ -83,7 +96,8 @@ function dbf_show_moreinfo(trainElem, keep_old) {
$.get(window.location.href, {train: trainElem.data('train'), jid: trainElem.data('jid'), ajax: 1}, function(data) {
$('.moreinfo').html(data);
}).fail(function() {
- $('.moreinfo .mfooter').append('Der Zug ist abgefahren (Zug nicht gefunden)');
+ $('.moreinfo .mfooter').append('Keine weiteren Details verfügbar');
+ $('.moreinfo .loading').remove();
});
infoElem.removeClass('collapsed-moreinfo');
infoElem.addClass('expanded-moreinfo');
@@ -94,21 +108,32 @@ function dbf_reg_handlers() {
$('div.app > ul > li').click(function(event) {
const trainElem = $(this);
const station = $('div.app').data('station');
+ const param = new URLSearchParams(window.location.search);
event.preventDefault();
var suffix = '?';
- if (window.location.href.includes('detailed=1')) {
+ if (param.get('detailed')) {
suffix += '&detailed=1';
}
- if (window.location.href.includes('hafas=1')) {
- suffix += '&hafas=1&highlight=' + trainElem.data('station');
+ if (param.get('dbris') && param.get('dbris') != '0') {
+ suffix += '&dbris=' + param.get('dbris') + '&highlight=' + trainElem.data('station');
}
- if (window.location.href.includes('past=1')) {
+ if (param.get('efa') && param.get('efa') != '0') {
+ suffix += '&efa=' + param.get('efa') + '&highlight=' + trainElem.data('station');
+ }
+ if (param.get('hafas') && param.get('hafas') != '0') {
+ suffix += '&hafas=' + param.get('hafas') + '&highlight=' + trainElem.data('station');
+ }
+ if (param.get('past')) {
suffix += '&past=1';
}
- if (window.location.href.includes('rt=1') || window.location.href.includes('show_realtime=1')) {
+ if (param.get('rt') || param.get('show_realtime')) {
suffix += '&rt=1';
}
- if (window.location.href.includes('hafas=1')) {
+ if (param.get('hafas') && param.get('hafas') != '0') {
+ history.pushState({'page':'traindetail','jid':trainElem.data('jid')}, 'test', '/z/' + trainElem.data('jid') + suffix);
+ } else if (param.get('efa') && param.get('efa') != '0') {
+ history.pushState({'page':'traindetail','jid':trainElem.data('jid')}, 'test', '/z/' + trainElem.data('jid') + suffix);
+ } else if (param.get('dbris') && param.get('dbris') != '0') {
history.pushState({'page':'traindetail','jid':trainElem.data('jid')}, 'test', '/z/' + trainElem.data('jid') + suffix);
} else {
history.pushState({'page':'traindetail','station':station,'train':trainElem.data('no')}, 'test', '/z/' + trainElem.data('train') + '/' + trainElem.data('station') + suffix);
diff --git a/public/static/js/dbf.min.js b/public/static/js/dbf.min.js
index c0048b5..f977bbd 100644
--- a/public/static/js/dbf.min.js
+++ b/public/static/js/dbf.min.js
@@ -1 +1 @@
-function reload_app(){0==$(".expanded-moreinfo").length?$.get(window.location.href,{ajax:1},function(e){$("div.app > ul").html(e),dbf_reg_handlers(),setTimeout(reload_app,6e4)}).fail(function(){setTimeout(reload_app,1e4)}):setTimeout(reload_app,3e4)}function dbf_show_moreinfo(d,n){const s=d.data("routeprev").split("|"),r=d.data("routenext").split("|"),l=d.data("moreinfo").split("|");$(".moreinfo").each(function(){var e=$(this);if(!n){$(".moreinfo .train-line").removeClass("sbahn fern ext ubahn bus tram").addClass(d.data("linetype")),$(".moreinfo .train-line").text(d.data("line")),$(".moreinfo .train-no").text(d.data("no")),$(".moreinfo .train-origin").text(d.data("from")),$(".moreinfo .train-dest").text(d.data("to")),$(".moreinfo .minfo").text(""),$(".moreinfo .mfooter").html(""),$(".moreinfo .verbose").html(""),$(".moreinfo .mroute").html(""),$(".moreinfo ul").html("");var a="";if(""!=d.data("arrival")?a+='<div><div class="arrival">An: '+d.data("arrival")+"</div></div>":a+='<div><div class="arrival"></div></div>',""!=d.data("platform")?a+='<div><div class="platform">Gleis '+d.data("platform")+"</div></div>":a+='<div><div class="platform"></div></div>',""!=d.data("departure")?a+='<div><div class="departure">Ab: '+d.data("departure")+"</div></div>":a+='<div><div class="departure"></div></div>',$(".moreinfo .mfooter").append('<div class="dataline">'+a+"</div>"),0==$(".moreinfo .loading").length&&$(".moreinfo .mfooter").append('<div class="loading">Lade Daten, bitte warten...</div>'),""!=d.data("moreinfo")){var t="";for(o in l)t+="<li>"+l[o]+"</li>";$(".moreinfo .mfooter").append("Meldungen: <ul>"+t+"</ul>")}var i="";if(""!=d.data("routeprev"))for(var o in s)i+="<li>"+s[o]+"</li>";if(i+="<li><strong>"+document.title+"</strong></li>",""!=d.data("routenext"))for(var o in r)i+="<li>"+r[o]+"</li>";$(".moreinfo .mfooter").append('Fahrtverlauf: <ul class="mroute">'+i+"</ul>")}$.get(window.location.href,{train:d.data("train"),jid:d.data("jid"),ajax:1},function(e){$(".moreinfo").html(e)}).fail(function(){$(".moreinfo .mfooter").append("Der Zug ist abgefahren (Zug nicht gefunden)")}),e.removeClass("collapsed-moreinfo"),e.addClass("expanded-moreinfo")})}function dbf_reg_handlers(){$("div.app > ul > li").click(function(e){var a=$(this),t=$("div.app").data("station"),e=(e.preventDefault(),"?");window.location.href.includes("detailed=1")&&(e+="&detailed=1"),window.location.href.includes("hafas=1")&&(e+="&hafas=1&highlight="+a.data("station")),window.location.href.includes("past=1")&&(e+="&past=1"),(window.location.href.includes("rt=1")||window.location.href.includes("show_realtime=1"))&&(e+="&rt=1"),window.location.href.includes("hafas=1")?history.pushState({page:"traindetail",jid:a.data("jid")},"test","/z/"+a.data("jid")+e):history.pushState({page:"traindetail",station:t,train:a.data("no")},"test","/z/"+a.data("train")+"/"+a.data("station")+e),dbf_show_moreinfo(a,!1)});const a=$(location).attr("hash").substr(1);var t;a&&(t=!1,$("div.app > ul > li").each(function(e){t||$(this).find(".anchor").each(function(){$(this).attr("id")==a&&(t=!0)})}),t)&&(t=!1,$("div.app > ul > li").each(function(e){t||($(this).find(".anchor").each(function(){$(this).attr("id")==a&&(t=!0)}),t?$(this).addClass("selected"):$(this).addClass("past"))}))}$(function(){$(".moresettings-header").each(function(){$(this).click(function(){var e=$(".moresettings");$(this).hasClass("moresettings-header-collapsed")?($(this).removeClass("moresettings-header-collapsed"),$(this).addClass("moresettings-header-expanded"),e.removeClass("moresettings-collapsed"),e.addClass("moresettings-expanded")):($(this).removeClass("moresettings-header-expanded"),$(this).addClass("moresettings-header-collapsed"),e.removeClass("moresettings-expanded"),e.addClass("moresettings-collapsed"))})}),$(".developers-header").each(function(){$(this).click(function(){var e=$(".developers");$(this).hasClass("developers-header-collapsed")?($(this).removeClass("developers-header-collapsed"),$(this).addClass("developers-header-expanded"),e.removeClass("developers-collapsed"),e.addClass("developers-expanded")):($(this).removeClass("developers-header-expanded"),$(this).addClass("developers-header-collapsed"),e.removeClass("developers-expanded"),e.addClass("developers-collapsed"))})}),dbf_reg_handlers(),$(".content .app").length&&(setTimeout(reload_app,3e4),history.replaceState({page:"station"},document.title,"")),window.onpopstate=function(a){var t;null!=a.state?"station"==a.state.page?($(".moreinfo").each(function(){$(this).removeClass("expanded-moreinfo"),$(this).addClass("collapsed-moreinfo")}),$("div.app > ul").length||($("div.app").append("<ul></ul>"),reload_app())):"traindetail"==a.state.page&&(t=!1,$("div.app > ul > li").each(function(){var e=$(this);e.data("no")==a.state.train&&(dbf_show_moreinfo(e,!0),t=!0)}),t||($(".moreinfo").each(function(){$(this).removeClass("collapsed-moreinfo"),$(this).addClass("expanded-moreinfo")}),$(".moreinfo .mfooter").append("Der Zug ist abgefahren (Zug nicht gefunden)"))):console.log("unhandled popstate! "+document.location)}});
+function setLang(e){document.cookie="lang="+e+";SameSite=None;Secure",location.reload()}function setTheme(e){localStorage.setItem("theme",e),otherTheme.hasOwnProperty(e)||(e=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"),addStyleSheet(e,"theme")}function reload_app(){0==$(".expanded-moreinfo").length?$.get(window.location.href,{ajax:1},function(e){$("div.app > ul").html(e),dbf_reg_handlers(),setTimeout(reload_app,6e4)}).fail(function(){setTimeout(reload_app,1e4)}):setTimeout(reload_app,3e4)}function dbf_show_moreinfo(d,s){const n=d.data("routeprev").split("|"),r=d.data("routenext").split("|"),l=d.data("moreinfo").split("|");$(".moreinfo").each(function(){var e=$(this);if(!s){$(".moreinfo .train-line").removeClass("sbahn fern ext ubahn bus tram").addClass(d.data("linetype")),$(".moreinfo .train-line").text(d.data("line")),$(".moreinfo .train-no").text(d.data("no")),$(".moreinfo .train-origin").text(d.data("from")),$(".moreinfo .train-dest").text(d.data("to")),$(".moreinfo .minfo").text(""),$(".moreinfo .mfooter").html(""),$(".moreinfo .verbose").html(""),$(".moreinfo .mroute").html(""),$(".moreinfo ul").html("");var a="";if(""!=d.data("arrival")?a+='<div><div class="arrival">An: '+d.data("arrival")+"</div></div>":a+='<div><div class="arrival"></div></div>',""!=d.data("platform")?a+='<div><div class="platform">Gleis '+d.data("platform")+"</div></div>":a+='<div><div class="platform"></div></div>',""!=d.data("departure")?a+='<div><div class="departure">Ab: '+d.data("departure")+"</div></div>":a+='<div><div class="departure"></div></div>',$(".moreinfo .mfooter").append('<div class="dataline">'+a+"</div>"),0==$(".moreinfo .loading").length&&$(".moreinfo .mfooter").append('<div class="loading">Lade Daten, bitte warten...</div>'),""!=d.data("moreinfo")){var t="";for(o in l)t+="<li>"+l[o]+"</li>";$(".moreinfo .mfooter").append("Meldungen: <ul>"+t+"</ul>")}var i="";if(""!=d.data("routeprev"))for(var o in n)i+="<li>"+n[o]+"</li>";if(i+="<li><strong>"+document.title+"</strong></li>",""!=d.data("routenext"))for(var o in r)i+="<li>"+r[o]+"</li>";$(".moreinfo .mfooter").append('Fahrtverlauf: <ul class="mroute">'+i+"</ul>")}$.get(window.location.href,{train:d.data("train"),jid:d.data("jid"),ajax:1},function(e){$(".moreinfo").html(e)}).fail(function(){$(".moreinfo .mfooter").append("Keine weiteren Details verfügbar"),$(".moreinfo .loading").remove()}),e.removeClass("collapsed-moreinfo"),e.addClass("expanded-moreinfo")})}function dbf_reg_handlers(){$("div.app > ul > li").click(function(e){var a=$(this),t=$("div.app").data("station"),i=new URLSearchParams(window.location.search),e=(e.preventDefault(),"?");i.get("detailed")&&(e+="&detailed=1"),i.get("dbris")&&"0"!=i.get("dbris")&&(e+="&dbris="+i.get("dbris")+"&highlight="+a.data("station")),i.get("efa")&&"0"!=i.get("efa")&&(e+="&efa="+i.get("efa")+"&highlight="+a.data("station")),i.get("hafas")&&"0"!=i.get("hafas")&&(e+="&hafas="+i.get("hafas")+"&highlight="+a.data("station")),i.get("past")&&(e+="&past=1"),(i.get("rt")||i.get("show_realtime"))&&(e+="&rt=1"),i.get("hafas")&&"0"!=i.get("hafas")||i.get("efa")&&"0"!=i.get("efa")||i.get("dbris")&&"0"!=i.get("dbris")?history.pushState({page:"traindetail",jid:a.data("jid")},"test","/z/"+a.data("jid")+e):history.pushState({page:"traindetail",station:t,train:a.data("no")},"test","/z/"+a.data("train")+"/"+a.data("station")+e),dbf_show_moreinfo(a,!1)});const a=$(location).attr("hash").substr(1);var t;a&&(t=!1,$("div.app > ul > li").each(function(e){t||$(this).find(".anchor").each(function(){$(this).attr("id")==a&&(t=!0)})}),t)&&(t=!1,$("div.app > ul > li").each(function(e){t||($(this).find(".anchor").each(function(){$(this).attr("id")==a&&(t=!0)}),t?$(this).addClass("selected"):$(this).addClass("past"))}))}$(function(){$(".moresettings-header").each(function(){$(this).click(function(){var e=$(".moresettings");$(this).hasClass("moresettings-header-collapsed")?($(this).removeClass("moresettings-header-collapsed"),$(this).addClass("moresettings-header-expanded"),e.removeClass("moresettings-collapsed"),e.addClass("moresettings-expanded")):($(this).removeClass("moresettings-header-expanded"),$(this).addClass("moresettings-header-collapsed"),e.removeClass("moresettings-expanded"),e.addClass("moresettings-collapsed"))})}),$(".developers-header").each(function(){$(this).click(function(){var e=$(".developers");$(this).hasClass("developers-header-collapsed")?($(this).removeClass("developers-header-collapsed"),$(this).addClass("developers-header-expanded"),e.removeClass("developers-collapsed"),e.addClass("developers-expanded")):($(this).removeClass("developers-header-expanded"),$(this).addClass("developers-header-collapsed"),e.removeClass("developers-expanded"),e.addClass("developers-collapsed"))})}),dbf_reg_handlers(),$(".content .app").length&&(setTimeout(reload_app,3e4),history.replaceState({page:"station"},document.title,"")),window.onpopstate=function(a){var t;null!=a.state?"station"==a.state.page?($(".moreinfo").each(function(){$(this).removeClass("expanded-moreinfo"),$(this).addClass("collapsed-moreinfo")}),$("div.app > ul").length||($("div.app").append("<ul></ul>"),reload_app())):"traindetail"==a.state.page&&(t=!1,$("div.app > ul > li").each(function(){var e=$(this);e.data("no")==a.state.train&&(dbf_show_moreinfo(e,!0),t=!0)}),t||($(".moreinfo").each(function(){$(this).removeClass("collapsed-moreinfo"),$(this).addClass("expanded-moreinfo")}),$(".moreinfo .mfooter").append("Der Zug ist abgefahren (Zug nicht gefunden)"))):console.log("unhandled popstate! "+document.location)}});
diff --git a/public/static/js/geostop.js b/public/static/js/geostop.js
index 80e8311..69bb607 100644
--- a/public/static/js/geostop.js
+++ b/public/static/js/geostop.js
@@ -39,10 +39,17 @@ $(function() {
const eva = candidate.eva,
name = candidate.name,
distance = candidate.distance.toFixed(1),
+ efa = candidate.efa,
hafas = candidate.hafas;
const stationlink = $(document.createElement('a'));
- stationlink.attr('href', eva + '?hafas=' + hafas);
+ if (efa) {
+ stationlink.attr('href', eva + '?efa=' + efa);
+ } else if (hafas) {
+ stationlink.attr('href', eva + '?hafas=' + hafas);
+ } else {
+ stationlink.attr('href', eva);
+ }
stationlink.text(name + ' ');
const distancenode = $(document.createElement('div'));
@@ -51,7 +58,7 @@ $(function() {
const icon = $(document.createElement('i'));
icon.attr('class', 'material-icons');
- icon.text(hafas ? 'directions' : 'train');
+ icon.text((hafas || efa) ? 'directions' : 'train');
stationlink.append(icon);
stationlink.append(distancenode);
@@ -61,7 +68,8 @@ $(function() {
};
const processLocation = function(loc) {
- $.post('/_geolocation', {lon: loc.coords.longitude, lat: loc.coords.latitude}, processResult).fail(function(jqXHR, textStatus, errorThrown) {
+ const param = new URLSearchParams(window.location.search);
+ $.post('/_geolocation', {lon: loc.coords.longitude, lat: loc.coords.latitude, efa: param.get('efa'), hafas: param.get('hafas')}, processResult).fail(function(jqXHR, textStatus, errorThrown) {
removeStatus();
showError("Netzwerkfehler: ", textStatus, errorThrown);
});
diff --git a/public/static/js/geostop.min.js b/public/static/js/geostop.min.js
index 8a5db00..5998966 100644
--- a/public/static/js/geostop.min.js
+++ b/public/static/js/geostop.min.js
@@ -1 +1 @@
-$(function(){function t(e){o(),e.error?r("Backend-Fehler:",e.error,null):0==e.candidates.length?r("Keine Stationen in 70km Umkreis gefunden","",null):$.each(e.candidates,function(e,t){var n=t.eva,o=t.name,r=t.distance.toFixed(1),t=t.hafas,a=$(document.createElement("a")),n=(a.attr("href",n+"?hafas="+t),a.text(o+" "),$(document.createElement("div"))),o=(n.attr("class","distance"),n.text(r),$(document.createElement("i")));o.attr("class","material-icons"),o.text(t?"directions":"train"),a.append(o),a.append(n),$("div.candidatelist").append(a)})}const o=function(){$("div.candidatestatus").remove()},r=function(e,t,n){var o=$(document.createElement("div")),t=(o.attr("class","error"),o.text(t),$(document.createElement("strong")));t.text(e),o.prepend(t),n&&((e=$(document.createElement("div"))).attr("class","errcode"),e.text(n),o.append(e)),$("div.candidatelist").append(o)};navigator.geolocation?(navigator.geolocation.getCurrentPosition(function(e){$.post("/_geolocation",{lon:e.coords.longitude,lat:e.coords.latitude},t).fail(function(e,t,n){o(),r("Netzwerkfehler: ",t,n)}),$("div.candidatestatus").text("Suche Stationen…")},function(e){o(),e.code==e.PERMISSION_DENIED?r("Standortanfrage nicht möglich.","Vermutlich fehlen die Rechte im Browser oder der Android Location Service ist deaktiviert.","geolocation.error.PERMISSION_DENIED"):e.code==e.POSITION_UNAVAILABLE?r("Standort konnte nicht ermittelt werden","(Service nicht verfügbar)","geolocation.error.POSITION_UNAVAILABLE"):e.code==e.TIMEOUT?r("Standort konnte nicht ermittelt werden","(Timeout)","geolocation.error.TIMEOUT"):r("Standort konnte nicht ermittelt werden","(unbekannter Fehler)","unknown geolocation.error code")}),$("div.candidatestatus").text("Position wird bestimmt…")):(o(),r("Standortanfragen werden von diesem Browser nicht unterstützt","",null))});
+$(function(){function n(e){a(),e.error?r("Backend-Fehler:",e.error,null):0==e.candidates.length?r("Keine Stationen in 70km Umkreis gefunden","",null):$.each(e.candidates,function(e,t){var n=t.eva,a=t.name,r=t.distance.toFixed(1),o=t.efa,t=t.hafas,i=$(document.createElement("a")),n=(o?i.attr("href",n+"?efa="+o):t?i.attr("href",n+"?hafas="+t):i.attr("href",n),i.text(a+" "),$(document.createElement("div"))),a=(n.attr("class","distance"),n.text(r),$(document.createElement("i")));a.attr("class","material-icons"),a.text(t||o?"directions":"train"),i.append(a),i.append(n),$("div.candidatelist").append(i)})}const a=function(){$("div.candidatestatus").remove()},r=function(e,t,n){var a=$(document.createElement("div")),t=(a.attr("class","error"),a.text(t),$(document.createElement("strong")));t.text(e),a.prepend(t),n&&((e=$(document.createElement("div"))).attr("class","errcode"),e.text(n),a.append(e)),$("div.candidatelist").append(a)};navigator.geolocation?(navigator.geolocation.getCurrentPosition(function(e){var t=new URLSearchParams(window.location.search);$.post("/_geolocation",{lon:e.coords.longitude,lat:e.coords.latitude,efa:t.get("efa"),hafas:t.get("hafas")},n).fail(function(e,t,n){a(),r("Netzwerkfehler: ",t,n)}),$("div.candidatestatus").text("Suche Stationen…")},function(e){a(),e.code==e.PERMISSION_DENIED?r("Standortanfrage nicht möglich.","Vermutlich fehlen die Rechte im Browser oder der Android Location Service ist deaktiviert.","geolocation.error.PERMISSION_DENIED"):e.code==e.POSITION_UNAVAILABLE?r("Standort konnte nicht ermittelt werden","(Service nicht verfügbar)","geolocation.error.POSITION_UNAVAILABLE"):e.code==e.TIMEOUT?r("Standort konnte nicht ermittelt werden","(Timeout)","geolocation.error.TIMEOUT"):r("Standort konnte nicht ermittelt werden","(unbekannter Fehler)","unknown geolocation.error code")}),$("div.candidatestatus").text("Position wird bestimmt…")):(a(),r("Standortanfragen werden von diesem Browser nicht unterstützt","",null))});
diff --git a/public/static/js/map-refresh.js b/public/static/js/map-refresh.js
index aa5629b..fcaac86 100644
--- a/public/static/js/map-refresh.js
+++ b/public/static/js/map-refresh.js
@@ -68,7 +68,15 @@ function dbf_anim_fine() {
}
function dbf_map_reload() {
- $.get('/_ajax_mapinfo/' + j_reqid, function(data) {
+ const param = new URLSearchParams(window.location.search);
+
+ const new_params = new URLSearchParams();
+ new_params.set('dbris', param.get('dbris') ?? '');
+ new_params.set('motis', param.get('motis') ?? '');
+ new_params.set('efa', param.get('efa') ?? '');
+ new_params.set('hafas', param.get('hafas') ?? '');
+
+ $.get('/_ajax_mapinfo/' + j_reqid + '?' + new_params.toString(), function(data) {
$('#infobox').html(data);
dbf_map_parse();
setTimeout(dbf_map_reload, 61000);
diff --git a/public/static/js/map-refresh.min.js b/public/static/js/map-refresh.min.js
index 215074b..745c922 100644
--- a/public/static/js/map-refresh.min.js
+++ b/public/static/js/map-refresh.min.js
@@ -1 +1 @@
-var j_reqid,j_positions=[],j_frame=[],j_frame_i=[];function dbf_map_parse(){$("#jdata").each(function(){j_reqid=$(this).data("req");var a=$(this).data("poly");if(a)for(var e in a=a.split("|"),j_positions=[],a){e=a[e].split(";");e[0]=parseFloat(e[0]),e[1]=parseFloat(e[1]),j_positions.push(e)}})}function dbf_anim_coarse(){if(j_positions.length){var a=marker.getLatLng(),e=a.lat,i=a.lng,a=j_positions.shift(),_=a[0],t=a[1];j_frame_i=200,j_frame=[];for(var f=1;f<=60;f++){var r=f/60;j_frame.push([e+(_-e)*r,i+(t-i)*r])}j_frame_i=0}}function dbf_anim_fine(){j_frame[j_frame_i]&&marker.setLatLng(j_frame[j_frame_i++])}function dbf_map_reload(){$.get("/_ajax_mapinfo/"+j_reqid,function(a){$("#infobox").html(a),dbf_map_parse(),setTimeout(dbf_map_reload,61e3)}).fail(function(){setTimeout(dbf_map_reload,5e3)})}$(document).ready(function(){$("#infobox").length&&(dbf_map_parse(),setInterval(dbf_anim_coarse,2e3),setInterval(dbf_anim_fine,33),setTimeout(dbf_map_reload,61e3))});
+var j_reqid,j_positions=[],j_frame=[],j_frame_i=[];function dbf_map_parse(){$("#jdata").each(function(){j_reqid=$(this).data("req");var a=$(this).data("poly");if(a)for(var e in a=a.split("|"),j_positions=[],a){e=a[e].split(";");e[0]=parseFloat(e[0]),e[1]=parseFloat(e[1]),j_positions.push(e)}})}function dbf_anim_coarse(){if(j_positions.length){var a=marker.getLatLng(),e=a.lat,t=a.lng,a=j_positions.shift(),i=a[0],r=a[1];j_frame_i=200,j_frame=[];for(var _=1;_<=60;_++){var f=_/60;j_frame.push([e+(i-e)*f,t+(r-t)*f])}j_frame_i=0}}function dbf_anim_fine(){j_frame[j_frame_i]&&marker.setLatLng(j_frame[j_frame_i++])}function dbf_map_reload(){var a=new URLSearchParams(window.location.search),e=new URLSearchParams;e.set("dbris",a.get("dbris")??""),e.set("motis",a.get("motis")??""),e.set("efa",a.get("efa")??""),e.set("hafas",a.get("hafas")??""),$.get("/_ajax_mapinfo/"+j_reqid+"?"+e.toString(),function(a){$("#infobox").html(a),dbf_map_parse(),setTimeout(dbf_map_reload,61e3)}).fail(function(){setTimeout(dbf_map_reload,5e3)})}$(document).ready(function(){$("#infobox").length&&(dbf_map_parse(),setInterval(dbf_anim_coarse,2e3),setInterval(dbf_anim_fine,33),setTimeout(dbf_map_reload,61e3))});
diff --git a/public/static/v84 b/public/static/v109
index 945c9b4..945c9b4 120000
--- a/public/static/v84
+++ b/public/static/v109
diff --git a/public/static/v85 b/public/static/v110
index 945c9b4..945c9b4 120000
--- a/public/static/v85
+++ b/public/static/v110
diff --git a/sass/app.scss b/sass/app.scss
index 8c0c075..75074bd 100644
--- a/sass/app.scss
+++ b/sass/app.scss
@@ -30,6 +30,7 @@ a {
p,
div.about,
+div.config,
div.input-field,
div.notes {
max-width: 94%;
@@ -37,6 +38,13 @@ div.notes {
margin-right: auto;
}
+div.journey,
+div.nextstop {
+ max-width: 98%;
+ margin-left: auto;
+ margin-right: auto;
+}
+
p {
text-align: justify;
}
@@ -142,6 +150,9 @@ div.content {
background-color: $powercar-wagon-color;
}
+ .closed {
+ background-color: $closed-wagon-color;
+ }
.nondestwagon {
border-style: dashed;
@@ -154,13 +165,29 @@ div.content {
.type {
display: inline-block;
width: 5em;
- color: $fg2;
+ color: $fg;
}
a.type {
color: $link-color;
}
+ .groupno {
+ color: $fg;
+ }
+
+ .grouptype {
+ color: $fg2;
+ }
+
+ .grouptype:before {
+ content: "(";
+ }
+
+ .grouptype:after {
+ content: ")";
+ }
+
.uicunknown {
color: $fg3;
}
@@ -191,7 +218,7 @@ div.content {
font-weight: bold;
}
- .uic78::before {
+ .uic78:before {
content: "-";
}
@@ -209,7 +236,7 @@ div.content {
color: $fg3;
}
- .uiccheck::before {
+ .uiccheck:before {
content: "-";
}
}
@@ -264,6 +291,9 @@ div.app {
&.cancelled {
background-color: $cancelled-bg-color;
+ .time {
+ color: $fg !important;
+ }
}
&.past {
@@ -325,6 +355,7 @@ div.app {
right: 7em;
height: 1.5em;
overflow: hidden;
+ white-space: nowrap;
}
.route {
@@ -342,6 +373,7 @@ div.app {
bottom:0;
left:4em;
width: 70%;
+ white-space: nowrap;
overflow: hidden;
color: $fg;
}
@@ -360,6 +392,12 @@ div.app {
}
}
+ .load {
+ color: $fg;
+ font-weight: normal;
+ margin-right: 0.5em;
+ }
+
.platform {
background-color: transparent;
font-size: 3em;
@@ -390,6 +428,16 @@ div.app {
background-color: transparent;
}
+ &.a-bit-delayed {
+ color: $smalldelay-color;
+ background-color: transparent;
+ }
+
+ &.on-time {
+ color: $ontime-color;
+ background-color: transparent;
+ }
+
.no-realtime {
background-color: transparent;
padding-right: 1ex;
@@ -520,13 +568,32 @@ div.app {
width: 100%;
display: flex;
justify-content: space-between;
- margin-bottom: 1em;
+ margin-bottom: 0.5em;
> div {
width: 33%;
}
}
+ .wagonorder-preview {
+ font-size: 110%;
+ width: 100%;
+ text-align: center;
+ margin-bottom: 1em;
+
+ a {
+ color: $fg;
+ }
+
+ .otherno {
+ color: $fg2;
+ }
+
+ .meta {
+ color: $fg1;
+ }
+ }
+
.departure {
text-align: right;
}
@@ -601,6 +668,45 @@ div.app {
list-style-type: circle;
}
+ .time-early {
+ color: $early-stop-color;
+ }
+
+ .time-delayed {
+ color: $delayed-stop-color;
+ }
+
+ .time-sched-only {
+ color: $delayed-stop-color;
+ }
+
+ .time-sched-ontime {
+ color: $early-stop-color;
+ }
+
+
+ .annotation {
+ color: $fg2;
+ list-style-type: none;
+ padding-left: 3em;
+ }
+
+ .-sched:before {
+ content: " ";
+ }
+
+ .time-sched:after {
+ content: " ";
+ }
+
+ .time-sched-only:before {
+ content: "(";
+ }
+
+ .time-sched-only:after {
+ content: ")";
+ }
+
i.material-icons {
font-size: 14px;
}
@@ -667,15 +773,27 @@ div.candidatelist a .traininfo {
padding-bottom: 0.3em;
}
-div.about {
+div.config {
margin-top: 2em;
font-family: Sans-Serif;
color: $fg2;
+
+ a {
+ color: $link-color;
+ cursor: pointer;
+ text-decoration: none;
+ }
}
-div.about a {
- color: $link-color;
- text-decoration: none;
+div.about {
+ margin-top: 1em;
+ font-family: Sans-Serif;
+ color: $fg2;
+
+ a {
+ color: $link-color;
+ text-decoration: none;
+ }
}
.notice {
@@ -887,6 +1005,10 @@ input[type="submit"]:active,
box-shadow: inset 0 3px 5px rgba(0,0,0,.125);
}
+.button-active {
+ font-weight: bold;
+}
+
.button-light {
color: $fg1;
background-color: $bg;
@@ -902,6 +1024,10 @@ input[type="submit"]:active,
border-color: $button-hover-border;
}
+div.backendlink {
+ margin-top: 1ex;
+}
+
div.notes {
margin-top: 2em;
}
diff --git a/sass/dark.scss b/sass/dark.scss
index 72a6927..78b61b0 100644
--- a/sass/dark.scss
+++ b/sass/dark.scss
@@ -32,18 +32,24 @@ $route-color: #dddddd;
$info-color: #ff7777;
$delay-color: #ff7777;
+$smalldelay-color: #dd9999;
$undelay-color: #77ff77;
$delaynorm-color: #dd9999;
$undelaynorm-color: #99dd99;
+$ontime-color: #aaeeaa;
$additional-stop-color: #77ff77;
$cancelled-stop-color: #ff7777;
+$early-stop-color: #ccffcc;
+$delayed-stop-color: #ff9999;
+
$cancelled-bg-color: #512f00;
$past-bg-color: $bg05;
$firstclass-wagon-color: #333300;
$powercar-wagon-color: #222222;
+$closed-wagon-color: #222222;
$button-hover: #111111;
$button-hover-border: #333333;
diff --git a/sass/light.scss b/sass/light.scss
index 809c8ce..60981b1 100644
--- a/sass/light.scss
+++ b/sass/light.scss
@@ -32,18 +32,24 @@ $route-color: #444444;
$info-color: #ff0000;
$delay-color: #ff0000;
+$smalldelay-color: #bb3333;
$undelay-color: #006600;
$delaynorm-color: #bb3333;
$undelaynorm-color: #338833;
+$ontime-color: #227722;
$additional-stop-color: #009900;
$cancelled-stop-color: #cc0000;
+$early-stop-color: #007700;
+$delayed-stop-color: #990000;
+
$cancelled-bg-color: #ffe7d0;
$past-bg-color: $bg05;
$firstclass-wagon-color: #ffff99;
$powercar-wagon-color: #cccccc;
+$closed-wagon-color: #dddddd;
$button-hover: #e6e6e6;
$button-hover-border: #adadad;
diff --git a/scripts/asset-release b/scripts/asset-release
index 5714745..418477f 100755
--- a/scripts/asset-release
+++ b/scripts/asset-release
@@ -5,7 +5,7 @@
set -ex
-current="$(find public/static/v* | tail -n 1 | grep -o '..$')"
+current="$(find public/static/v* | tail -n 1 | grep -o '...$')"
prev=$((current - 1))
next=$((current + 1))
diff --git a/templates/_intersection_infobox.html.ep b/templates/_intersection_infobox.html.ep
deleted file mode 100644
index cb27d19..0000000
--- a/templates/_intersection_infobox.html.ep
+++ /dev/null
@@ -1,22 +0,0 @@
-<div class="container" id="infobox2" style="margin-top: 1ex; margin-bottom: 1ex;">
-<div class="journey" id="jdata"
-data-req="<%= stash('ajax_req') %>"
-data-route="<%= stash('ajax_route') %>"
-data-poly="<%= stash('ajax_polyline') %>"
->
- <strong><%= stash('train1_no') %></strong>
- und
- <strong><%= stash('train2_no') %></strong>
- werden sich wahrscheinlich
- % if (my $t = stash('time')) {
- gegen <strong><%= $t->strftime('%H:%M') %> Uhr</strong>
- % }
- % if (my $p = stash('likely_pair')) {
- zwischen <strong><%= $p->[0] %></strong> und <strong><%= $p->[1] %></strong>
- % }
- % if (not stash('time')) {
- nicht
- % }
- begegnen.
-</div>
-</div>
diff --git a/templates/_map_infobox.html.ep b/templates/_map_infobox.html.ep
index 42e121d..16625f5 100644
--- a/templates/_map_infobox.html.ep
+++ b/templates/_map_infobox.html.ep
@@ -1,6 +1,6 @@
<div class="container" id="infobox" style="margin-top: 1ex; margin-bottom: 1ex;">
<div class="journey" id="jdata"
-data-req="<%= stash('ajax_req') %>"
+data-req="<%= stash('ajax_req') =~ s{#}{%23}gr %>"
data-route="<%= stash('ajax_route') %>"
data-poly="<%= stash('ajax_polyline') %>"
>
diff --git a/templates/_train_details.html.ep b/templates/_train_details.html.ep
index 2ed3dd4..2c18da2 100644
--- a/templates/_train_details.html.ep
+++ b/templates/_train_details.html.ep
@@ -2,11 +2,8 @@
<div>
% if ($departure->{train_no} or $departure->{train_line}) {
<span class="train-line <%= $linetype %>"><%= $departure->{train_type} %>
-% if ($linetype eq 'fern' and $icetype and $icetype->[1]) {
- <span class="trainsubtype" aria-hidden="true"><%= $icetype->[1] %></span>
-% }
-
- <%= $departure->{train_line} // $departure->{train_no} %></span>
+ %= $departure->{train_line} // $departure->{train_no}
+ </span>
<span class="train-no"><%= $departure->{train_line} ? $departure->{train_no} : q{} %></span>
% }
</div>
@@ -30,7 +27,7 @@
% }
% elsif ($departure->{arrival}) {
% if ($departure->{arrival} ne $departure->{sched_arrival}) {
- % if (($departure->{delay} // 0) < 0) {
+ % if (($departure->{arrival_delay} // 0) < 0) {
An: <span class="undelay"><%= $departure->{arrival} %></span>
% }
% else {
@@ -48,6 +45,9 @@
% elsif ($departure->{prep_time}) {
Ein: <%= $departure->{prep_time} %>
% }
+% if ($departure->{tz_offset} and $departure->{local_sched_arr}) {
+ <br/>Lokal: <%= $departure->{local_sched_arr}->strftime('%H:%M') %>
+% }
</div>
</div>
<div>
@@ -68,10 +68,10 @@
% else {
% my $left = '';
% my $right = '';
-% if ($departure->{direction} and $departure->{direction} eq 'l') {
+% if ($departure->{wr_direction} and $departure->{wr_direction} =~ m{l}) {
% $left = '◀ ';
% }
-% elsif ($departure->{direction} and $departure->{direction} eq 'r') {
+% elsif ($departure->{wr_direction} and $departure->{wr_direction} =~ m{r}) {
% $right = ' ▶';
% }
% if ($departure->{scheduled_platform} and $departure->{platform}
@@ -110,30 +110,55 @@
% elsif ($departure->{sched_departure}) {
Ab: <%= $departure->{sched_departure} %>
% }
+% if ($departure->{tz_offset} and $departure->{local_sched_dep}) {
+ <br/>Lokal: <%= $departure->{local_sched_dep}->strftime('%H:%M') %>
+% }
</div>
</div>
</div> <!-- dataline -->
+% if (my $wr = $departure->{wr}) {
+ <div class="wagonorder-preview">
+% my $left = defined $wr->direction ? $wr->direction == 100 ? q{} : '←' : q{};
+% my $right = defined $wr->direction ? $wr->direction == 100 ? '→' : q{} : q{};
+% if ($departure->{wr_direction} and $departure->{wr_direction} =~ m{l}) {
+% $left = '◀';
+% $right = q{};
+% }
+% elsif ($departure->{wr_direction} and $departure->{wr_direction} =~ m{r}) {
+% $left = q{};
+% $right = '▶';
+% }
+ <a href="/carriage-formation?<%= $departure->{wr_link} %>&amp;e=<%= $departure->{wr_direction} // '' %>">
+ %= $left
+ % for my $entry ((defined $departure->{wr_direction_num} and $departure->{wr_direction_num} != $wr->direction) ? reverse @{$departure->{wr_preview} // []} : @{$departure->{wr_preview} // []}) {
+ % if ($entry->[1]) {
+ <span class="<%= $entry->[1] %>"><%= $entry->[0] %></span>
+ % }
+ % else {
+ %= $entry->[0]
+ % }
+ % }
+ %= $right
+ </a>
+ </div>
+% }
<div class="verbose">
% if ($departure->{trip_id}) {
% if (stash('station_name')) {
- <a class="smallbutton" href="/map/<%= $departure->{trip_id} %>/<%= $departure->{train_line} // 0 %>?from=<%= stash('station_name') %>"><i class="material-icons" aria-hidden="true">map</i> Karte</a>
+ <a class="smallbutton" href="/map/<%= $departure->{trip_id} =~ s{#}{%23}gr %>/<%= $departure->{train_line} || 0 %>?from=<%= stash('station_name') %>&amp;dbris=<%= param('dbris') %>&amp;efa=<%= param('efa') // q{} %>&amp;hafas=<%= param('hafas') // q{} %>"><i class="material-icons" aria-hidden="true">map</i> Karte</a>
% }
% else {
- <a class="smallbutton" href="/map/<%= $departure->{trip_id} %>/<%= $departure->{train_line} // 0 %>"><i class="material-icons" aria-hidden="true">map</i> Karte</a>
+ <a class="smallbutton" href="/map/<%= $departure->{trip_id} =~ s{#}{%23}gr %>/<%= $departure->{train_line} || 0 %>?dbris=<%= param('dbris') %>&amp;efa=<%= param('efa') // q{} %>&amp;hafas=<%= param('hafas') // q{} %>"><i class="material-icons" aria-hidden="true">map</i> Karte</a>
% }
% }
% if ($departure->{wr_link}) {
- <a class="smallbutton" href="/_wr/<%= $departure->{train_no} %>/<%= $departure->{wr_link} %>?e=<%= $departure->{direction} // '' %>"><i class="material-icons" aria-hidden="true">train</i> Wagen
+ <a class="smallbutton" href="/carriage-formation?<%= $departure->{wr_link} %>&amp;e=<%= $departure->{wr_direction} // '' %>"><i class="material-icons" aria-hidden="true">train</i> <%= $departure->{wr_text} || 'Wagen' %>
</a>
% }
-% elsif ($icetype and $icetype->[2] and ($linetype eq 'fern' or $departure->{train_type} =~ m{NJ})) {
- <a class="smallbutton" href="/wr/<%= $departure->{train_no} %>"><i class="material-icons" aria-hidden="true">train</i> Plan: <%= $icetype->[0] %></a>
-% }
-% elsif ($icetype and $icetype->[1] and $linetype eq 'fern') {
- <span class="disabledbutton"><i class="material-icons" aria-hidden="true">train</i> Plan: <%= $icetype->[0] %></span>
-% }
-% if ($departure->{train_type} and $departure->{train_no}) {
- <a class="smallbutton" href="https://bahn.expert/details/<%= $departure->{train_type} %>%20<%= $departure->{train_no} %>/<%= ($departure->{start} // DateTime->now(time_zone => 'Europe/Berlin'))->iso8601 %>?evaNumberAlongRoute=<%= $departure->{eva} %>"><img src="/static/icons/bahn-expert.svg">Details</a>
+% if ($departure->{trip_id} and param('dbris') and param('dbris') eq 'bahn.de') {
+ <a class="smallbutton" href="https://bahn.expert/details/x/h/<%= Mojo::Util::url_escape( $departure->{trip_id} ) %>"><img src="/static/icons/bahn-expert.svg">Details</a>
+% } elsif ($departure->{train_type} and $departure->{train_no} and (not param('hafas') or param('hafas') eq 'DB')) {
+ <a class="smallbutton" href="https://bahn.expert/details/<%= $departure->{train_type} %>%20<%= $departure->{train_no} %>/<%= ($departure->{date} // DateTime->now(time_zone => 'Europe/Berlin'))->iso8601 %>?evaNumberAlongRoute=<%= $departure->{eva} %>"><img src="/static/icons/bahn-expert.svg">Details</a>
% }
% for my $link (@{$departure->{links}}) {
<a class="smallbutton" href="<%= $link->[1] %>"><i class="material-icons" aria-hidden="true">warning</i> <%= $link->[0] %></a>
@@ -167,7 +192,7 @@
% }
% if ($departure->{moreinfo} and @{$departure->{moreinfo}}) {
- Meldungen:
+ Meldungen
<ul class="messages">
% for my $pair (@{$departure->{moreinfo}}) {
<li>
@@ -203,11 +228,33 @@
</ul>
% }
% if ($departure->{route_pre_diff} and $departure->{route_post_diff}) {
- Fahrtverlauf:
+% if ($departure->{date}) {
+ Fahrtverlauf am
+% if (stash('train') !~ m{[|]}) {
+ <a href="<%= url_for('train', train => stash('train'))->query({detailed => param('detailed'), past => param('past'), rt => param('rt'), hafas => param('hafas'), date => $departure->{date}->clone->subtract(days => 1)->strftime('%d.%m.%Y'), highlight => param('highlight') // stash('station')}) %>">◀</a>
+% }
+%= $departure->{date}->strftime('%d.%m.%Y')
+% if (stash('train') !~ m{[|]}) {
+ <a href="<%= url_for('train', train => stash('train'))->query({detailed => param('detailed'), past => param('past'), rt => param('rt'), hafas => param('hafas'), date => $departure->{date}->clone->add(days => 1)->strftime('%d.%m.%Y'), highlight => param('highlight') // stash('station')}) %>">▶</a>
+% }
+% }
<ul class="mroute">
% for my $stop (@{$departure->{route_pre_diff}}) {
+% if ($stop->{is_annotated} and $stop->{prod_name}) {
+ <li class="annotation">
+% if ($stop->{prod_name}) {
+%= $stop->{prod_name}
+% }
+% if ($stop->{direction}) {
+ → <%= $stop->{direction} %>
+% }
+% if ($stop->{operator}) {
+ (<%= $stop->{operator} %>)
+% }
+ </li>
+% }
<li class="<%= $stop->{isPast} ? 'past-stop' : 'future-stop' %>">
- <a href="<%= url_for('station', station => $stop->{eva} // $stop->{name})->query({detailed => param('detailed'), past => param('past'), rt => param('rt'), hafas => param('hafas')}) %>#<%= ($departure->{train_type} // q{x}) . ($departure->{train_no} // q{x}) %>" class="
+ <a href="<%= url_for('station', station => $stop->{eva} // $stop->{name})->query({detailed => param('detailed'), past => param('past'), rt => param('rt'), dbris => param('dbris'), efa => param('efa'), hafas => param('hafas')}) %>#<%= ((param('dbris') or param('hafas')) and $departure->{trip_id}) ? ($departure->{trip_id} =~ s{[ #|]}{x}gr) : (($departure->{train_type} // q{x}) . ($departure->{train_no} // q{x})) %>" class="
% if ($stop->{isAdditional}) {
additional-stop
% }
@@ -221,44 +268,88 @@
generic-stop
% }
% if (($stop->{rt_dep} and $stop->{dep_delay}) or (not $stop->{rt_dep} and $stop->{rt_arr} and $stop->{arr_delay})) {
- "><%= ($stop->{sched_dep} // $stop->{sched_arr})->strftime('%H:%M') %> (heute <%= ($stop->{rt_dep} // $stop->{rt_arr})->strftime('%H:%M') %>)
+ "><span class="time-sched-only"><%= ($stop->{sched_dep} // $stop->{sched_arr})->strftime('%H:%M') %></span> <span class="time-delayed"><%= ($stop->{rt_dep} // $stop->{rt_arr})->strftime('%H:%M') %></span>
+% }
+% elsif (($stop->{rt_dep} and defined $stop->{dep_delay}) or (not $stop->{rt_dep} and $stop->{rt_arr} and defined $stop->{arr_delay})) {
+ "><span class="time-sched-ontime"><%= ($stop->{sched_dep} // $stop->{sched_arr}) ? ($stop->{sched_dep} // $stop->{sched_arr})->strftime('%H:%M') : q{} %></span>
% }
% else {
- "><%= ($stop->{sched_dep} // $stop->{sched_arr}) ? ($stop->{sched_dep} // $stop->{sched_arr})->strftime('%H:%M') : q{} %>
-% if ($stop->{rt_bogus}) {
- <i class="material-icons" aria-label="Echtzeitdaten fehlen">gps_off</i>
-% }
+ "><span class="time-sched"><%= ($stop->{sched_dep} // $stop->{sched_arr}) ? ($stop->{sched_dep} // $stop->{sched_arr})->strftime('%H:%M') : q{} %></span>
+% }
+% if ($stop->{tz_offset} and $stop->{local_dt_da}) {
+ (lokal <%= $stop->{local_dt_da}->strftime('%H:%M') %>)
% }
<%= $stop->{name} %></a>
% if ($stop->{load}{FIRST} or $stop->{load}{SECOND}) {
% my ($text, $icon1, $icon2) = utilization_icon([$stop->{load}{FIRST}, $stop->{load}{SECOND}]);
- <i class="material-icons" aria-hidden="true"><%= $icon1 %></i>
+ % if ($icon1 ne 'help_outline') {
+ <i class="material-icons" aria-hidden="true"><%= $icon1 %></i>
+ % }
<i class="material-icons" aria-hidden="true"><%= $icon2 %></i>
% }
</li>
% }
% if (stash('station_name')) {
- <li class="<%= $departure->{is_cancelled} ? 'cancelled-stop' : q{} %> <%= $departure->{isPast} ? 'past-stop' : 'future-stop' %>"><%= $departure->{sched_departure} // $departure->{sched_arrival} // q{} %>
+% if ($departure->{is_annotated} and $departure->{prod_name}) {
+ <li class="annotation">
+% if ($departure->{prod_name}) {
+%= $departure->{prod_name}
+% }
+% if ($departure->{direction}) {
+ → <%= $departure->{direction} %>
+% }
+% if ($departure->{operator}) {
+ (<%= $departure->{operator} %>)
+% }
+ </li>
+% }
+ <li class="<%= $departure->{is_cancelled} ? 'cancelled-stop' : q{} %> <%= $departure->{isPast} ? 'past-stop' : 'future-stop' %>">
% if ($departure->{departure} and $departure->{sched_departure} and $departure->{departure} ne $departure->{sched_departure}) {
- (heute <%= $departure->{departure} %>)
+ <span class="time-sched-only"><%= $departure->{sched_departure} // $departure->{sched_arrival} // q{} %></span><span class="time-delayed">
+% }
+% elsif ($departure->{departure} and $departure->{sched_departure} and $departure->{departure} eq $departure->{sched_departure} and not $departure->{no_realtime_yet}) {
+ <span class="time-sched-ontime">
% }
% elsif ($departure->{arrival} and $departure->{sched_arrival} and $departure->{arrival} ne $departure->{sched_arrival}) {
- (heute <%= $departure->{arrival} %>)
+ <span class="time-sched-only"><%= $departure->{sched_departure} // $departure->{sched_arrival} // q{} %></span><span class="time-delayed">
% }
-% if ($departure->{missing_realtime} or $departure->{no_realtime_yet}) {
- <i class="material-icons" aria-label="Echtzeitdaten fehlen">gps_off</i>
+% elsif ($departure->{arrival} and $departure->{sched_arrival} and $departure->{arrival} eq $departure->{sched_arrival} and not $departure->{no_realtime_yet}) {
+ <span class="time-sched-ontime">
+% }
+% else {
+ <span class="time-sched">
+% }
+%= $departure->{departure} // $departure->{arrival} // $departure->{sched_departure} // $departure->{sched_arrival} // q{}
+ </span>
+% if ($departure->{tz_offset} and $departure->{local_dt_da}) {
+ (lokal <%= $departure->{local_dt_da}->strftime('%H:%M') %>)
% }
<strong><%= stash('station_name') %></strong>
% if (my $u = $departure->{utilization}) {
% my ($text, $icon1, $icon2) = utilization_icon($u);
- <i class="material-icons" aria-hidden="true"><%= $icon1 %></i>
+ % if ($icon1 ne 'help_outline') {
+ <i class="material-icons" aria-hidden="true"><%= $icon1 %></i>
+ % }
<i class="material-icons" aria-hidden="true"><%= $icon2 %></i>
% }
</li>
% }
% for my $stop (@{$departure->{route_post_diff}}) {
+% if ($stop->{is_annotated} and $stop->{prod_name}) {
+ <li class="annotation">
+% if ($stop->{prod_name}) {
+%= $stop->{prod_name}
+% }
+% if ($stop->{direction}) {
+ → <%= $stop->{direction} %>
+% }
+% if ($stop->{operator}) {
+ (<%= $stop->{operator} %>)
+% }
+ </li>
+% }
<li class="<%= $stop->{isPast} ? 'past-stop' : 'future-stop' %>">
- <a href="<%= url_for('station', station => $stop->{eva} // $stop->{name})->query({detailed => param('detailed'), past => param('past'), rt => param('rt'), hafas => param('hafas')}) %>#<%= ($departure->{train_type} // q{x}) . ($departure->{train_no} // q{x}) %>" class="
+ <a href="<%= url_for('station', station => $stop->{eva} // $stop->{name})->query({detailed => param('detailed'), past => param('past'), rt => param('rt'), dbris => param('dbris'), efa => param('efa'), hafas => param('hafas')}) %>#<%= ((param('dbris') or param('hafas')) and $departure->{trip_id}) ? ($departure->{trip_id} =~ s{[ #|]}{x}gr) : (($departure->{train_type} // q{x}) . ($departure->{train_no} // q{x})) %>" class="
% if ($stop->{isAdditional}) {
additional-stop
% }
@@ -272,26 +363,31 @@
generic-stop
% }
% if (($stop->{rt_arr} and $stop->{arr_delay}) or (not $stop->{rt_arr} and $stop->{rt_dep} and $stop->{dep_delay})) {
- "><%= ($stop->{sched_arr} // $stop->{sched_dep})->strftime('%H:%M') %> (heute <%= ($stop->{rt_arr} // $stop->{rt_dep})->strftime('%H:%M') %>)
+ "><span class="time-sched-only"><%= ($stop->{sched_arr} // $stop->{sched_dep})->strftime('%H:%M') %></span> <span class="time-delayed"><%= ($stop->{rt_arr} // $stop->{rt_dep})->strftime('%H:%M') %></span>
+% }
+% elsif (($stop->{rt_arr} and defined $stop->{arr_delay}) or (not $stop->{rt_arr} and $stop->{rt_dep} and defined $stop->{dep_delay})) {
+ "><span class="time-sched-ontime"><%= ($stop->{sched_arr} // $stop->{sched_dep}) ? ($stop->{sched_arr} // $stop->{sched_dep})->strftime('%H:%M') : q{} %></span>
% }
% else {
- "><%= ($stop->{sched_arr} // $stop->{sched_dep}) ? ($stop->{sched_arr} // $stop->{sched_dep})->strftime('%H:%M') : q{} %>
-% if ($stop->{rt_bogus}) {
- <i class="material-icons" aria-label="Echtzeitdaten fehlen">gps_off</i>
-% }
+ "><span class="time-sched"><%= ($stop->{sched_arr} // $stop->{sched_dep}) ? ($stop->{sched_arr} // $stop->{sched_dep})->strftime('%H:%M') : q{} %></span>
+% }
+% if ($stop->{tz_offset} and $stop->{local_dt_ad}) {
+ (lokal <%= $stop->{local_dt_ad}->strftime('%H:%M') %>)
% }
<%= $stop->{name} %></a>
% if ($stop->{load}{FIRST} or $stop->{load}{SECOND}) {
% my ($text, $icon1, $icon2) = utilization_icon([$stop->{load}{FIRST}, $stop->{load}{SECOND}]);
- <i class="material-icons" aria-hidden="true"><%= $icon1 %></i>
+ % if ($icon1 ne 'help_outline') {
+ <i class="material-icons" aria-hidden="true"><%= $icon1 %></i>
+ % }
<i class="material-icons" aria-hidden="true"><%= $icon2 %></i>
% }
</li>
% }
</ul> <!-- mroute -->
% }
-% if ($departure->{operator}) {
- <div class="details">Betrieb: <%= $departure->{operator} %></div>
+% if ($departure->{operators} and @{$departure->{operators} // []}) {
+ <div class="details">Betrieb: <%= join(q{, }, @{ $departure->{operators} // [] } ) %></div>
% }
% if ($departure->{details} and @{$departure->{details}}) {
<div class="details">Details:
@@ -316,71 +412,4 @@
</ul>
</div>
% }
-% if ($details->{attributes}) {
-% if (@{$details->{attributes}} > 1) {
- <div class="db-attr">
- Attribute:
- <ul>
-% for my $attr (@{$details->{attributes}}) {
- <li><%= include '_train_attr', attr => $attr, with_station => 1 %></li>
-% }
- </ul>
- </div>
-% }
-% else {
- <div class="db-attr">
-%= include '_train_attr', attr => $details->{attributes}[0], with_station => 0
- </div>
-% }
-% }
-% if ($details and not $departure->{arrival}) {
-% if (my $s = $details->{route}{preStart}) {
- Zug wird voraussichtlich aus <%= $s %> eingesetzt.<br/><br/>
-% }
-% if (@{$departure->{cycle_from} // []}) {
- Bildung möglicherweise aus
- <ul>
-% for my $t (@{$departure->{cycle_from}}) {
-% my ($train_no, $train) = @{$t};
-% my $tt = $train->{type} // $train->{rawType} // 'Zug';
-% $tt =~ s{ .*|[0-9]}{};
-% if ($tt ne 'Zug') {
- <li><a href="<%= url_for('train', train => "$tt $train_no")->query({detailed => param('detailed'), past => param('past'), rt => param('rt'), hafas => param('hafas')}) %>"><%= $tt %> <%= $train_no %></a>
-% }
-% else {
- <li><%= $tt %> <%= $train_no %>
-% }
-% if ($train->{route}{start} and $train->{route}{end}) {
- <%= $train->{route}{start} %> → <%= $train->{route}{end} %>
-% }
- </li>
-% }
- </ul>
-% }
-% }
-% if ($details and not $departure->{departure}) {
-% if (my $e = $details->{route}{postEnd}) {
- Zug wird voraussichtlich in <%= $e %> abgestellt.<br/><br/>
-% }
-% if (@{$departure->{cycle_to} // []}) {
- Weiterfahrt möglicherweise als
- <ul>
-% for my $t (@{$departure->{cycle_to}}) {
-% my ($train_no, $train) = @{$t};
-% my $tt = $train->{type} // $train->{rawType} // 'Zug';
-% $tt =~ s{ .*|[0-9]}{};
-% if ($tt ne 'Zug') {
- <li><a href="<%= url_for('train', train => "$tt $train_no")->query({detailed => param('detailed'), past => param('past'), rt => param('rt'), hafas => param('hafas')}) %>"><%= $tt %> <%= $train_no %></a>
-% }
-% else {
- <li><%= $tt %> <%= $train_no %>
-% }
-% if ($train->{route}{start} and $train->{route}{end}) {
- <%= $train->{route}{start} %> → <%= $train->{route}{end} %>
-% }
- </li>
-% }
- </ul>
-% }
-% }
</div> <!-- mfooter -->
diff --git a/templates/_wagon.html.ep b/templates/_wagon.html.ep
index 94ef56a..dccecc0 100644
--- a/templates/_wagon.html.ep
+++ b/templates/_wagon.html.ep
@@ -1,36 +1,36 @@
% my $bg = '';
% my $extra_class = '';
-% if ($wagon->is_first_class) {
+% if ($wagon->has_first_class) {
% $extra_class .= ' firstclass';
% }
% if ($wagon->is_locomotive or $wagon->is_powercar) {
% $extra_class .= ' powercar';
% }
-% if ($wagon->train_no ne $train_no) {
+% if ($wagon->is_closed) {
+% $extra_class .= ' closed';
+% }
+% if ($group->train_no ne $train_no) {
% $extra_class .= ' nondestwagon';
% }
<div class="wagon <%= $extra_class %>" style="
- top: <%= $wagon->{position}{start_percent} %>%; bottom: <%= 100 - $wagon->{position}{end_percent} %>%; <%= $bg %>">
+ top: <%= $wagon->start_percent %>%; bottom: <%= 100 - $wagon->end_percent %>%; <%= $bg %>">
% if ($wagon->is_locomotive or $wagon->is_powercar) {
% }
+% elsif ($wagon->is_closed) {
+ X
+% }
% else {
-%= $wagon->number // '?'
-% if ($wagon->has_accessibility) {
+%= $wagon->number // q{}
+% if ($wagon->has_wheelchair_space) {
<i class="material-icons" style="font-size: 20px;">accessible</i>
% }
% if ($wagon->has_bistro) {
<i class="material-icons">restaurant</i>
% }
-% if ($wagon->has_compartments) {
- <!--<i class="material-icons">folder</i>-->
-% }
-% if ($wagon->has_quiet_area) {
+% if ($wagon->has_quiet_zone) {
<i class="tiny material-icons">volume_off</i>
% }
-% if ($wagon->has_phone_area) {
- <i class="material-icons">smartphone</i>
-% }
-% if ($wagon->has_family_area) {
+% if ($wagon->has_family_zone) {
<i class="material-icons">people</i>
% }
% if ($wagon->has_bahn_comfort) {
@@ -38,9 +38,9 @@
% }
% }
<div class="direction">
-% if (not defined $direction) {
+% if (not defined $wr->direction) {
% }
-% elsif ($direction == 100) {
+% elsif ($wr->direction == 100) {
<i class="material-icons">arrow_downward</i>
% }
% else {
@@ -49,9 +49,9 @@
</div>
</div>
<div class="details" style="
- top: <%= $wagon->{position}{start_percent} %>%; bottom: <%= 100 - $wagon->{position}{end_percent} %>%;">
+ top: <%= $wagon->start_percent %>%; bottom: <%= 100 - $wagon->end_percent %>%;">
% if ($exit_dir ne 'right') {
-% if (my $img = wagon_image($wagon->train_subtype // $type // '?', $wagon->type, $wagon->uic_id)) {
+% if (my $img = wagon_image($wr->train_type // '?', $wagon->type, $wagon->uic_id)) {
<a class="type" href="/w/<%= $img %>?n=<%= $wagon->number // '' %>&amp;s=<%= $wagon->section %>&amp;r=<%= $wref %>"><%= $wagon->type %></a>
% }
% else {
@@ -61,7 +61,7 @@
% }
% }
% my $uic_id = $wagon->uic_id;
-% if (length($uic_id) != 12) {
+% if (length($uic_id) != 12 and length($uic_id) != 14) {
<span class="uicunknown"><%= $uic_id %></span>
% }
% elsif (substr($uic_id, 0, 2) >= 90) {
@@ -71,7 +71,7 @@
<span class="uicexchange"><%= substr($uic_id, 0, 2) %></span><span class="uiccountry"><%= substr($uic_id, 2, 2) %></span><span class="uic56"><%= substr($uic_id, 4, 2) %></span><span class="uic78"><%= substr($uic_id, 6, 2) %></span><span class="uicno"><%= substr($uic_id, 8, 3) %></span><span class="uiccheck"><%= substr($uic_id, 11) %></span>
% }
% if ($exit_dir eq 'right') {
-% if (my $img = wagon_image($wagon->train_subtype // $type // '?', $wagon->type, $wagon->uic_id)) {
+% if (my $img = wagon_image($wr->train_type // '?', $wagon->type, $wagon->uic_id)) {
<a class="type" href="/w/<%= $img %>?n=<%= $wagon->number // '' %>&amp;s=<%= $wagon->section %>&amp;r=<%= $wref %>"><%= $wagon->type %></a>
% }
% else {
@@ -80,4 +80,18 @@
</span>
% }
% }
+% if ($multi and $first) {
+ <br/>
+ <span class="groupno">
+% if (scalar $wr->train_numbers > 1) {
+ <%= $group->train_type %> <%= $group->train_no %>
+% }
+% if (scalar $wr->destinations > 1) {
+ → <%= $group->destination %>
+% }
+ </span>
+ % if ($multi and $group->desc_short) {
+ <span class="grouptype"><%= $group->desc_short %></span>
+% }
+% }
</div>
diff --git a/templates/about.html.ep b/templates/about.html.ep
index f299389..3bf8295 100644
--- a/templates/about.html.ep
+++ b/templates/about.html.ep
@@ -1,48 +1,51 @@
<div class="container">
<p>
- DBF ist ein inoffizieller Abfahrtsmonitor für den Regional- und Fernverkehr mit dem Ziel, Daten aus verschiedenen Quellen übersichtlich zusammenzutragen.
- Der Fokus liegt auf Zügen im Netz der Deutschen Bahn; eingeschränkte Unterstützung für Nahverkehr und Züge in anderen Netzen lässt sich optional zuschalten.
+ DBF ist ein inoffizieller Abfahrtsmonitor für Nah-, Regional- und Fernverkehr in Deutschland und Umgebung.
+ Die Fahrten in der Übersicht verlinken je eine Detailseite mit Unterwegshalten, Meldungen und Kartendarstellung.
+ Bei HAFAS-Backends ist zusätzlich die Suche nach spezifischen Fahrten möglich.
</p>
<p>
- Der <a href="<%= app->config->{'source_url'} %>">Quelltext</a> steht unter der <a href="https://git.finalrewind.org/db-fakedisplay/tree/COPYING">GNU AGPL v3</a> als Open Source zur Verfügung. © 2011 – 2023 <a href="https://finalrewind.org">derf</a>.
+ Der <a href="<%= app->config->{'source_url'} %>">Quelltext</a> steht unter der <a href="https://git.finalrewind.org/db-fakedisplay/tree/COPYING">GNU AGPL v3</a> als Open Source zur Verfügung. © 2011 – 2024 <a href="https://finalrewind.org">derf</a>.
% if (my $issue_url = app->config->{'issue_url'}) {
Fehlermeldungen bitte via
<a href="<%= $issue_url %>">Issue Tracker</a>.
% }
+ Alle von DBF referenzierten Informationen können auch direkt per CLI im Text- oder JSON-Format abgerufen werden – die unten verlinkten Backends beinhalten entsprechende Anwendungen.
</p>
<p>
- Das Projekt begann als „db-fakedisplay“ (kurz dbf) zur <a href="/Dortmund
- Hbf?mode=multi">Nachahmung von Bahnhofs-Abfahrtstafeln</a>. Inzwischen
- liegt der Fokus auf dem <a href="/Dortmund Hbf">App/Infoscreen-Modus</a>
- und die Bezeichnung DBF wurde zum Eigennamen ohne weitere Bedeutung.
+ Diese Installation nutzt
+ <strong>DBF v<%= stash('version') // '???' %></strong> mit folgenden Backends:
+ <ul>
+ <li>Innerdeutscher Regional- und Fernverkehr: DB IRIS via <a href="https://finalrewind.org/projects/Travel-Status-DE-IRIS/">Travel::Status::DE::IRIS</a>
+ <strong>v<%= $Travel::Status::DE::IRIS::VERSION %></strong></li>
+ <li>Nah-, Regional- und Fernverkehr im In- und Ausland: bahn.de via <a href="https://finalrewind.org/projects/Travel-Status-DE-DBRIS/">Travel::Status::DE::DBRIS</a>
+ <strong>v<%= $Travel::Status::DE::DBRIS::VERSION %></strong></li>
+ <li>Nah-, Regional- und Fernverkehr im In- und Ausland: EFA via <a href="https://finalrewind.org/projects/Travel-Status-DE-VRR/">Travel::Status::DE::EFA</a>
+ <strong>v<%= $Travel::Status::DE::EFA::VERSION %></strong></li>
+ <li>Nah-, Regional- und Fernverkehr im In- und Ausland: HAFAS via <a href="https://finalrewind.org/projects/Travel-Status-DE-DeutscheBahn/">Travel::Status::DE::HAFAS</a>
+ <strong>v<%= $Travel::Status::DE::HAFAS::VERSION %></strong></li>
+ <li>Nah-, Regional- und Fernverkehr im In- und Ausland: MOTIS via <a href="https://finalrewind.org/projects/Travel-Status-MOTIS/">Travel::Status::MOTIS</a>
+ <strong>v<%= $Travel::Status::MOTIS::VERSION %></strong></li>
+ </ul>
</p>
<p>
- Diese Installation verwendet die DBF-Version
- <b><%= stash('version') // '???' %></b> und greift auf die folgenden Backends
- zu:<br/>
- • Regional- und Fernverkehr: DB IRIS via <a href="https://finalrewind.org/projects/Travel-Status-DE-IRIS/">Travel::Status::DE::IRIS</a>
- v<%= $Travel::Status::DE::IRIS::VERSION %><br/>
- • Nahverkehr und Zugdetails: DB HAFAS via <a href="https://finalrewind.org/projects/Travel-Status-DE-DeutscheBahn/">Travel::Status::DE::HAFAS</a>
- % if ($Travel::Status::DE::HAFAS::VERSION) {
- v<%= $Travel::Status::DE::HAFAS::VERSION %>
- % }
- <br/>
- • Wagenreihung: <a href="https://finalrewind.org/projects/Travel-Status-DE-DBWagenreihung/">Travel::Status::DE::DBWagenreihung</a>
- % if ($Travel::Status::DE::DBWagenreihung::VERSION) {
- v<%= $Travel::Status::DE::DBWagenreihung::VERSION %>
- % }
- <br/>
- • Zugauslastung Regionalverkehr: VRR EFA via <a href="https://github.com/derf/eva-to-efa-gw">eva-to-efa-gw</a><br/>
- <br/>
- Sie nutzt zusätzlich die folgenden Open Data-Ressourcen:<br/>
- • <a href="https://data.deutschebahn.com/dataset/zugbildungsplanzugbildungsplan-zpar">Zugbildungsplan</a> © DB Fernverkehr AG, lizensiert unter CC-BY 4.0
- <br/>
- • <a href="http://data.deutschebahn.com/dataset/data-haltestellen">Haltestellenliste</a>
+ Verwendete Ressourcen:
+ <ul>
+ <li><a href="/_backend">HAFAS-Backends</a> via <a href="https://github.com/public-transport/transport-apis">transport-apis</a>, CC0</li>
+ <li><a href="https://data.deutschebahn.com/dataset/zugbildungsplanzugbildungsplan-zpar">Zugbildungsplan</a> © DB Fernverkehr AG, lizensiert unter CC-BY 4.0</li>
+ <li><a href="http://data.deutschebahn.com/dataset/data-haltestellen">Haltestellenliste</a>
© DB Station&amp;Service AG,
Europaplatz 1,
- 10557 Berlin, lizensiert unter CC-BY 4.0<br/>
- • <a href="https://data.deutschebahn.com/dataset/fahrzeuglexikon">Fahrzeuglexikon</a>
- © DB Fernverkehr AG, lizensiert unter CC-BY 4.0; Abbildungen © Seemanngrafik d.i.p. im Auftrag der Deutschen Bahn AG, lizensiert unter CC-BY-SA 4.0<br/>
+ 10557 Berlin, lizensiert unter CC-BY 4.0</li>
+ <li><a href="https://data.deutschebahn.com/dataset/fahrzeuglexikon">Fahrzeuglexikon</a>
+ © DB Fernverkehr AG, lizensiert unter CC-BY 4.0; Abbildungen © Seemanngrafik d.i.p. im Auftrag der Deutschen Bahn AG, lizensiert unter CC-BY-SA 4.0</li>
+ </ul>
+ </p>
+ <p>
+ Trivia: Das Projekt begann als „db-fakedisplay“ (kurz dbf) zur
+ Nachahmung von Bahnhofs-Abfahrtstafeln. Inzwischen liegt der Fokus auf
+ der Bereitstellung von Informationen für mobile und Desktop-Anwendungen
+ und die Bezeichnung DBF wurde zum Eigennamen ohne weitere Bedeutung.
</p>
</div>
diff --git a/templates/app.html.ep b/templates/app.html.ep
index 360a2f0..8b52c61 100644
--- a/templates/app.html.ep
+++ b/templates/app.html.ep
@@ -30,8 +30,8 @@
% $route_str .= $stop . ($via_cur < $via_max ? ' - ' : q{});
% }
<li
-% if (param('hafas')) {
- data-jid="<%= $departure->{journey_id} %>"
+% if (param('dbris') or param('hafas') or param('efa')) {
+ data-jid="<%= $departure->{journey_id} =~ s{#}{%23}gr %>"
% }
data-train="<%= ($departure->{train_type} // q{}) %> <%= ($departure->{train_no} // $departure->{train} // q{}) %>"
data-line="<%= $departure->{train_type} %> <%= $departure->{train_line} // $departure->{train_no} %>"
@@ -53,12 +53,23 @@
>
% }
% if (param('hafas')) {
- <a href="/z/<%= Mojo::Util::url_escape($departure->{journey_id}) . '?hafas=1&highlight=' . Mojo::Util::url_escape($departure->{station} // $station) %>">
+ <a href="/z/<%= Mojo::Util::url_escape($departure->{journey_id}) . '?hafas=' . Mojo::Util::url_escape(param('hafas')) . '&amp;highlight=' . Mojo::Util::url_escape($departure->{station} // $station) %>">
+% }
+% elsif (param('efa')) {
+ <a href="/z/<%= Mojo::Util::url_escape($departure->{journey_id}) . '?efa=' . Mojo::Util::url_escape(param('efa')) . '&amp;highlight=' . Mojo::Util::url_escape($departure->{station} // $station) %>">
+% }
+% elsif (param('dbris')) {
+ <a href="/z/<%= Mojo::Util::url_escape($departure->{journey_id}) . '?dbris=' . Mojo::Util::url_escape(param('dbris')) . '&amp;highlight=' . Mojo::Util::url_escape($departure->{station} // $station) %>">
% }
% else {
<a href="/z/<%= Mojo::Util::url_escape(($departure->{train_type} // q{}) . ' ' . ($departure->{train_no} // $departure->{train} // q{})) . '/' . Mojo::Util::url_escape($departure->{station} // $station) %>">
% }
- <div class="anchor" id="<%= ($departure->{train_type} // q{x}) . ($departure->{train_no} // q{x}) %>"></div>
+% if (param('dbris') or param('hafas')) {
+ <div class="anchor" id="<%= $departure->{journey_id} =~ s{[ #|]}{x}gr %>"></div>
+% }
+% else {
+ <div class="anchor" id="<%= ($departure->{train_type} // q{x}) . ($departure->{train_no} // q{x}) %>"></div>
+% }
<div class="line <%= $departure->{linetype} %>">
% if ($departure->{train_type} and $departure->{train_no}) {
%= $departure->{train_type}
@@ -67,9 +78,6 @@
%= $departure->{train_line}
% }
% elsif ($departure->{train_no}) {
-% if (param('detailed') and $departure->{linetype} eq 'fern' and exists $ice_type->{$departure->{train_no}} and $ice_type->{$departure->{train_no}}[1]) {
- <span class="trainsubtype" aria-hidden="true"><%= $ice_type->{$departure->{train_no}}[1] %></span>
-% }
<span class="trainno"><%= $departure->{train_no} %></span>
% }
% else {
@@ -103,15 +111,14 @@
%= $departure->{origin}
</span>
% }
- <span class="time <%= ($show_realtime and $departure->{delay} and not
- $departure->{is_cancelled}) ? 'delayed' : q{} %>">
-% if ($departure->{delay} and not $departure->{is_cancelled}) {
-% if ($show_realtime) {
+ <span class="time <%= $show_realtime ? get_rt_time_class($departure) : q{} %>">
+% if ($departure->{delay} and not $departure->{is_cancelled} and not $departure->{departure_is_cancelled}) {
+% if ($show_realtime and ($departure->{sched_arrival} or $departure->{sched_departure})) {
% if ($departure->{delay} > ($hide_low_delay ? 4 : 0)) {
- <span class="delaynorm" aria-hidden="true">+<%= $departure->{delay} %> ⇒</span>
+ <span class="delaynorm" aria-hidden="true"><%= $departure->{sched_departure} // $departure->{sched_arrival} %> ⇒</span>
% }
% elsif ($departure->{delay} < 0) {
- <span class="undelaynorm" aria-hidden="true"><%= $departure->{delay} %> ⇒</span>
+ <span class="undelaynorm" aria-hidden="true"><%= $departure->{sched_departure} // $departure->{sched_arrival} %> ⇒</span>
% }
% }
% else {
@@ -143,7 +150,12 @@
% }
% }
% else {
-%= $departure->{time}
+% if ($departure->{is_cancelled} or $departure->{departure_is_cancelled}) {
+%= $departure->{sched_departure} // $departure->{sched_arrival} // $departure->{time}
+% }
+% else {
+%= $departure->{time}
+% }
% }
</span>
% if (($departure->{scheduled_platform} and $departure->{platform} and
@@ -154,6 +166,18 @@
% else {
<span class="platform">
% }
+% if ($departure->{load}{FIRST} or $departure->{load}{SECOND}) {
+% my ($text, $icon1, $icon2) = utilization_icon([$departure->{load}{FIRST}, $departure->{load}{SECOND}]);
+ <span class="load">
+ <i class="material-icons" style="vertical-align: bottom;" aria-hidden="true"><%= $icon2 %></i>
+ </span>
+% }
+% elsif (my $o = $departure->{occupancy}) {
+ <span class="load">
+% my ($text, $icon) = occupancy_icon($o);
+ <i class="material-icons" style="vertical-align: bottom;" aria-hidden="true"><%= $icon %></i>
+ </span>
+% }
<span class="visually-hidden">Gleis</span>
%= $departure->{platform}
</span>
diff --git a/templates/coverage_map.html.ep b/templates/coverage_map.html.ep
new file mode 100644
index 0000000..bd3d94c
--- /dev/null
+++ b/templates/coverage_map.html.ep
@@ -0,0 +1,22 @@
+<div class="container">
+ Das <%= $backend %>-Backend „<%= $service %>“ liefert ungefähr innerhalb
+ der folgenden grob umrissenen Region voraussichtlich nützliche Echtzeitdaten.
+</div>
+
+<div class="container">
+ <div id="map" style="height: 70vh;">
+ </div>
+</div>
+
+<script>
+const map = L.map('map').setView([51.306, 9.712], 6);
+
+L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+ attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
+}).addTo(map);
+
+const coverage = L.geoJSON(<%== $coverage %>);
+
+coverage.addTo(map);
+map.fitBounds(coverage.getBounds());
+</script>
diff --git a/templates/exception.html.ep b/templates/exception.html.ep
index 65ec7ff..7654c0b 100644
--- a/templates/exception.html.ep
+++ b/templates/exception.html.ep
@@ -5,7 +5,7 @@ Beim Bearbeiten der Anfrage ist ein Fehler aufgetreten.<br/>
<pre>
----------[Debug start]----------
% if ($exception) {
-%= $exception->message
+%= ref($exception) ? $exception->message : $exception
Stash:
%= dumper $snapshot
% }
diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep
index 4913586..80fd34f 100644
--- a/templates/landingpage.html.ep
+++ b/templates/landingpage.html.ep
@@ -1,18 +1,31 @@
% if (stash 'show_intro') {
<div class="container">
-<p>
- DBF ist ein inoffizieller Abfahrtsmonitor für den Regional- und Fernverkehr mit dem Ziel, Daten aus verschiedenen Quellen übersichtlich zusammenzutragen.
- Der Fokus liegt auf Zügen im Netz der Deutschen Bahn; eingeschränkte Unterstützung für Nahverkehr und Züge in anderen Netzen lässt sich optional zuschalten.
-</p>
-<p>
- Diese Seite ist ein kostenfreies, privat betriebenes Projekt ohne
- Verfügbarkeitsgarantie. Alle Angaben ohne Gewähr.
-</p>
+% if (0) {
+ <p>
+ DBF is an unofficial departure monitor for regional and long-distance trains within Germany, aiming to combine multiple data sources in a useful manner.
+ It also has limited support for local transit and traffic outside of Germany.
+ </p>
+ <p>
+ This site is operated by a private entity in a not-for-profit manner.
+ There are no uptime or reliability guarantees whatsoever.
+ </p>
+% }
+% else {
+ <p>
+ DBF ist ein inoffizieller Abfahrtsmonitor für Nah-, Regional- und Fernverkehr in Deutschland und Umgebung mit dem Ziel, Daten aus verschiedenen Quellen zusammenzutragen.
+ Es unterstützt neben Fahrten im Netz der DB InfraGO diverse Nah- und Fernverkehrsunternehmen mit EFA- und HAFAS-Backends.
+ Die Fahrten in der Übersicht verlinken je eine Detailseite mit Unterwegshalten, Meldungen und Kartendarstellung.
+ </p>
+ <p>
+ Diese Seite ist ein kostenfreies, privat betriebenes Projekt ohne Verfügbarkeitsgarantie.
+ Alle Angaben ohne Gewähr.
+ </p>
+% }
<p class="geolink">
-<a class="button" href="<%= url_for('_autostop')->to_abs->scheme('https') %>">Stationen in der Umgebung suchen</a>
+<a class="button" href="<%= url_for('_autostop')->to_abs->scheme('https')->query({efa => param('efa'), hafas => param('hafas')}) %>">Stationen in der Umgebung suchen</a>
</p>
<p>
-Oder hier angeben:
+Oder hier eine Station angeben:
</p>
</div>
% }
diff --git a/templates/layouts/app.html.ep b/templates/layouts/app.html.ep
index 18f129b..c557bee 100644
--- a/templates/layouts/app.html.ep
+++ b/templates/layouts/app.html.ep
@@ -4,8 +4,8 @@
<title><%= stash('title') // 'DBF' %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta name="keywords" content="Abfahrtsmonitor, Bahnhofstafel, Abfahrten, Abfahrtstafel, ICE, IC, RE, RB, S-Bahn">
- <meta name="description" content="<%= stash('description') // 'Inoffizieller Abfahrtsmonitor für innerdeutsche Zugfahrten' %>">
+ <meta name="keywords" content="Abfahrtsmonitor, Bahnhofstafel, Abfahrten, Abfahrtstafel, Nahverkehr, Regionalverkehr, Fernverkehr, ICE, IC, RE, RB, S-Bahn">
+ <meta name="description" content="<%= stash('description') // 'Inoffizieller Abfahrtsmonitor für Nah-, Reginol- und Fernverkehr' %>">
<meta name="theme-color" content="#00838f">
<link rel="icon" type="image/png" href="/static/icons/icon-16x16.png" sizes="16x16">
<link rel="icon" type="image/png" href="/static/icons/icon-32x32.png" sizes="32x32">
@@ -18,7 +18,7 @@
<meta http-equiv="refresh" content="<%= $self->stash('refresh_interval') %>"/>
% }
- % my $av = 'v85'; # asset version
+ % my $av = 'v110'; # asset version
% if (session('theme') and session('theme') eq 'dark' or param('dark')) {
%= stylesheet "/static/${av}/css/dark.min.css", id => 'theme'
% }
@@ -27,14 +27,14 @@
% }
<script>
function addStyleSheet(name, id) {
- var path = '/static/<%=$av%>/css/' + name + '.min.css';
- var old = document.getElementById(id);
+ const path = '/static/<%=$av%>/css/' + name + '.min.css';
+ const old = document.getElementById(id);
if (old && (old.href != path)) {
old.href = path;
- document.cookie = 'theme=' + name;
+ document.cookie = 'theme=' + name + ';SameSite=None;Secure';
}
}
- var otherTheme = {
+ const otherTheme = {
'dark': 'light',
'light': 'dark',
};
@@ -43,12 +43,6 @@
currentTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
}
addStyleSheet(currentTheme, 'theme');
-
- function toggleTheme() {
- currentTheme = otherTheme[currentTheme] || 'light';
- localStorage.setItem('theme', currentTheme);
- addStyleSheet(currentTheme, 'theme');
- }
</script>
%= stylesheet "/static/${av}/css/material-icons.css"
%= stylesheet "/static/${av}/css/jquery-ui.min.css"
@@ -88,9 +82,6 @@
<a href="<%= stash('api_link') %>"><span class="visually-hidden"><%= stash('api_text') %></span><i class="material-icons" aria-hidden="true"><%= stash('api_icon') %></i></a>
</li>
% }
- <li class="waves-effect waves-light">
- <a onClick="javascript:toggleTheme()"><span class="visually-hidden">Farbschema invertieren</span><i class="material-icons" aria-hidden="true">invert_colors</i></a>
- </li>
% if (stash('hide_opts')) {
<li><a href="/"><span class="visually-hidden">Hauptseite</span><i class="material-icons" aria-hidden="true">edit</i></a></li>
% }
@@ -126,42 +117,36 @@ Bitte eine Station aus der Liste auswählen</div>
%= form_for _redirect => begin
+%= hidden_field efa => param('efa')
+%= hidden_field hafas => param('hafas')
<div>
<div class="field">
- <div class="desc">Zug / Station</div>
- <div>
-% if (stash('stationlist')) {
+% if (stash('stationlist')) {
%= select_field input => stash('stationlist')
-% }
-% elsif (stash('input')) {
- %= text_field 'input', class => 'station', placeholder => 'Zug, Stationsname oder Ril100-Kürzel', id => 'stationinput'
-% }
-% else {
- %= text_field 'input', class => 'station', placeholder => 'Zug, Stationsname oder Ril100-Kürzel', id => 'stationinput', autofocus => 'autofocus'
-% }
- </div>
+% }
+% elsif (stash('input')) {
+ %= text_field 'input', class => 'station', placeholder => 'Stationsname oder Fahrtnummer', id => 'stationinput'
+% }
+% else {
+ %= text_field 'input', class => 'station', placeholder => 'Stationsname oder Fahrtnummer', id => 'stationinput', autofocus => 'autofocus'
+% }
</div>
<div class="field">
- %= submit_button 'Abfahrtsmonitor'
+ %= submit_button 'Abfahrtstafel'
</div>
% if (stash('input')) {
<div class="geolink">
- <a class="button" href="<%= url_for('_autostop')->to_abs->scheme('https') %>">Stationen in der Umgebung suchen</a>
+ <a class="button" href="<%= url_for('_autostop')->to_abs->scheme('https')->query({efa => param('efa'), hafas => param('hafas')}) %>">Stationen in der Umgebung suchen</a>
</div>
% }
+ <div class="backendlink">
+ <a class="button button-light" href="<%= url_for('_backend')->query({efa => param('efa'), hafas => param('hafas')}) %>">Backend: <%= param('efa') ? param('efa') . ' (EFA)' : param('hafas') ? param('hafas') . ' (HAFAS)' : 'DB (IRIS-TTS)' %></a>
+ </div>
<div class="break"></div>
<div class="moresettings-header moresettings-header-collapsed button button-light">Weitere Einstellungen</div>
<div class="moresettings moresettings-collapsed">
<div class="field">
<div class="desc">
- %= check_box 'rt' => 1, id => 'id_show_realtime'
- <label for="id_show_realtime">
- Zeiten inkl. Verspätung angeben
- </label>
- </div>
- </div>
- <div class="field">
- <div class="desc">
%= check_box 'hidelowdelay' => 1, id => 'id_hidelowdelay'
<label for="id_hidelowdelay">
Verspätungen erst ab 5 Minuten anzeigen
@@ -172,15 +157,7 @@ Bitte eine Station aus der Liste auswählen</div>
<div class="desc">
%= check_box 'detailed' => 1, id => 'id_detailed'
<label for="id_detailed">
- Mehr Details (u.a. Zugnummern und Zugbildungsplan)
- </label>
- </div>
- </div>
- <div class="field">
- <div class="desc">
- %= check_box 'no_related' => 1, id => 'id_no_related'
- <label for="id_no_related">
- Betriebliche Bahnhofstrennungen berücksichtigen (z.B. "Hbf (Fern+Regio)" vs. "Hbf (S)")
+ Mehr Details
</label>
</div>
</div>
@@ -188,7 +165,7 @@ Bitte eine Station aus der Liste auswählen</div>
<div class="desc">
%= check_box 'past' => 1, id => 'past'
<label for="past">
- Bereits abgefahrene Züge anzeigen
+ Fahrten der vergangenen 60 Minuten zeigen
</label>
</div>
</div>
@@ -196,13 +173,13 @@ Bitte eine Station aus der Liste auswählen</div>
<div class="desc">
%= check_box 'hide_opts' => 1, id => 'id_hide_opts'
<label for="id_hide_opts">
- Formular verstecken (für Infoscreens)
+ Formular verstecken
</label>
</div>
</div>
<div class="field">
<div class="desc">
- Nur Züge über
+ Nur Fahrten über
</div>
<div>
%= text_field 'via', placeholder => 'Bahnhof 1, Bhf2, ... (oder regulärer Ausdruck)', class => 'station'
@@ -242,32 +219,66 @@ Bitte eine Station aus der Liste auswählen</div>
</div> <!-- input-field -->
<div class="notes">
- <div class="developers-header developers-header-collapsed button button-light">API- und Entwickler-Hinweise</div>
+ <div class="developers-header developers-header-collapsed button button-light">API</div>
<div class="developers developers-collapsed">
<ul>
- <li>DBF-Abfahrtstafeln können gerne als iframe eingebunden oder in
- fest installierten Vollbild-Browserfenstern verwendet werden.
- Für eine kleine Ansicht (z.B. iframe in einer normalen Website)
- empfiehlt sich das "App"-Frontend. Für eine große Ansicht
- (z.B. als alleinstehender Infoscreen) gibt es den "Infoscreen"-Modus.</li>
- <li>Die Parameter <span style="font-family: monospace;">mode=json&amp;version=3</span>
- (alternativ <span style="font-family:
- monospace;">https://dbf.finalrewind.org/Bahnhofsname.json?version=3</span>)
- bieten ein JSON-IRIS-Interface. Die route-Elemente können zusätzlich
- die Felder "isAdditional" oder "isCancelled" enthalten, der Rest sollte
- selbsterklärend sein. Im Fehlerfall fehlt das "departures"-Element,
- stattdessen wird ein "error"-Element mit Fehlermeldung zurückgegeben.
- Bitte maximal 30 Anfragen pro Minute und insbesondere nur eine Anfrage
- pro Station und Minute – eine höhere Auflösung haben die Backenddaten
- ohnehin nicht.</li>
- <li>Ein JSON-Interface für Zugdetails ist in Arbeit.</li>
- <li>Mit <span style="font-family: monospace;">limit</span> kann die Anzahl der
- angezeigten / im JSON enthaltenen Abfahrten eingeschränkt werden, z.B.
- <span style="font-family: monospace;">limit=10</span> für die ersten zehn.</li>
- <li>Dieser Dienst ist Open Source-Software und kann leicht auf eigenen Servern
- <a href="https://github.com/derf/db-fakedisplay/blob/master/README.md">installiert</a>
- werden. Automatisierte Crawler, die mehrere Dutzend Stationen pro Minute
- abfragen, bitte nur auf eigenen Instanzen betreiben.</li>
+ % if (0) {
+ <li>You're welcome to embed DBF departure boards as iframes or use them
+ in full-screen browser setups. The App frontend works best for
+ small screens, whereas the legacy Infoscreen mode is better suited
+ for large displays.</li>
+ <li>The departure board supports names, EVA IDs, and (in IRIS mode)
+ DS100/Ril100 codes as station identifiers.</li>
+ <li>Requests for train details can optionally be suffixed with the
+ DD.MM.[YYYY] date of the requested trip, e.g. "ICE 921 (1.1.)" or
+ "ICE 921 @ 1.1.". The date refers to the scheduled departure at the
+ train's origin station.</li>
+ <li>A JSON IRIS API is avaliable via
+ <span style="font-family: monospace;">mode=json&amp;version=3</span>
+ (or just <span style="font-family: monospace;">https://dbf.finalrewind.org/Station.json?version=3</span>).
+ Route elements may contain "isAdditional" and "isCancelled"; the rest
+ should be self-explanatory. Please do not send more than 30 requests
+ per minute and only one request per station per minute.</li>
+ <li>There is no JSON API for train details yet.</li>
+ <li>The optional <span style="font-family: monospace;">limit</span>
+ parameter limits the number of returnd departures; e.g.
+ <span style="font-family: monospace;">limit=10</span> will result in no more than ten.</li>
+ <li>DBF is available as Open Source software
+ (<a href="https://github.com/derf/db-fakedisplay/blob/master/README.md">installation instructions</a>).
+ Please use your own installation for automated crawlers that request dozens of stations per minute.</li>
+ % }
+ % else {
+ <li>DBF-Abfahrtstafeln können gerne als iframe eingebunden oder in
+ fest installierten Vollbild-Browserfenstern verwendet werden.
+ Für eine kleine Ansicht (z.B. iframe in einer normalen Website)
+ empfiehlt sich das "App"-Frontend. Für eine große Ansicht
+ (z.B. als alleinstehender Infoscreen) gibt es den "Infoscreen"-Modus.</li>
+ <li>Die Abfahrtstafel unterstützt Namen, EVA-IDs, und (im IRIS-Backend)
+ DS100/Ril100-Codes zur Identifikation von Stationen.</li>
+ <li>Abfahrten werden mit Echtzeitdaten bzw. Prognosen angegeben und
+ danach sortiert. Mit dem Parameter
+ <span style="font-family: monospace;">rt=0</span> wwerden stattdessen
+ Plandaten angegeben und zur Sortierung genutzt.</li>
+ <li>Bei HAFAS-Backends können optional Details für spezifische Fahrten im
+ DD.MM.[YYYY]-Format abgefragt werden, z.B. "ICE 921 (1.1.)" oder
+ "ICE 921 @ 1.1.". Das Datum bezieht sich auf die geplante
+ Abfahrtszeit am Startbahnhof der Fahrt.</li>
+ <li>Viele Seiten sind auch als JSON verfügbar, wahlweise mittels
+ <span style="font-family: monospace;">Accept: application/json</span> oder
+ durch <span style="font-family: monospace;">.json</span> in der URL.
+ HAFAS- und IRIS-Abfahrtstafeln liefern mit dem GET-Parameter <span style="font-family: monospace;">version=3</span> eine stabile JSON-API.
+ Alle anderen Endpunkte (sowie Abfahrtstafeln mit <span style="font-family: monospace;">version=raw</span>) erlauben direkten Zugriff auf die serialisierten Travel::Status::DE::{EFA,HAFAS,IRIS}-Objekte ohne stabile API.</li>
+ <li>Bitte maximal 30 Anfragen pro Minute und insbesondere nur eine Anfrage
+ pro Station und Minute – eine höhere Auflösung haben die Backenddaten
+ ohnehin nicht.</li>
+ <li>Mit <span style="font-family: monospace;">limit</span> kann die Anzahl der
+ angezeigten / im JSON enthaltenen Abfahrten eingeschränkt werden, z.B.
+ <span style="font-family: monospace;">limit=10</span> für die ersten zehn.</li>
+ <li>Dieser Dienst ist Open Source-Software und kann leicht auf eigenen Servern
+ <a href="https://github.com/derf/db-fakedisplay/blob/master/README.md">installiert</a>
+ werden. Automatisierte Crawler, die mehrere Dutzend Stationen pro Minute
+ abfragen, bitte nur auf eigenen Instanzen betreiben.</li>
+ % }
</ul>
</div> <!-- developers -->
</div> <!-- notes -->
@@ -275,13 +286,32 @@ Bitte eine Station aus der Liste auswählen</div>
</div> <!-- container -->
<div class="container">
+<div class="config">
+Farbschema:
+<a onClick="javascript:setTheme('light')">hell</a>
+<a onClick="javascript:setTheme('dark')">dunkel</a>
+<a onClick="javascript:setTheme('default')">automatisch</a>
+<!--Language:
+<br/>
+<a onClick="javascript:setLang('de')">DE</a>
+<a onClick="javascript:setLang('en')">EN</a>
+<a onClick="javascript:setLang('default')">system language</a>
+-->
+</div> <!-- config -->
+</div> <!-- container -->
+% }
+% if (not stash('hide_footer')) {
+<div class="container">
<div class="about">
-<a href="_about">Über DBF</a>
+<a href="_about">DBF</a> v<%= stash('version') // '???' %>
·
<a href="_datenschutz" rel="nofollow">Datenschutz</a>
·
-<a href="_impressum" rel="nofollow">Impressum</a><br/>
-Version <%= stash('version') // '???' %>
+<a href="_impressum" rel="nofollow">Impressum</a>
</div> <!-- about -->
</div> <!-- container -->
% }
diff --git a/templates/layouts/legacy.html.ep b/templates/layouts/legacy.html.ep
index 5554557..e7e59ec 100644
--- a/templates/layouts/legacy.html.ep
+++ b/templates/layouts/legacy.html.ep
@@ -17,13 +17,13 @@
<meta http-equiv="refresh" content="<%= $self->stash('refresh_interval') %>"/>
% }
- % my $av = 'v85'; # asset version
- %= stylesheet "/static/${av}/css/default.css"
+ % my $av = 'v110'; # asset version
+ %= stylesheet "/static/${av}/css/legacy.css"
%= stylesheet "/static/${av}/css/material-icons.css"
%= stylesheet "/static/${av}/css/jquery-ui.min.css"
% my $force_mobile = param('force_mobile') // stash('force_mobile');
% if ($force_mobile) {
- %= stylesheet "/static/${av}/css/mobile.css"
+ %= stylesheet "/static/${av}/css/legacy-mobile.css"
% }
%if (stash('load_marquee')) {
%= javascript '/static/js/jquery-3.4.1.min.js'
@@ -62,169 +62,5 @@ Bitte eine Station aus der Liste auswählen</div>
%= content
</div>
-% if (not stash('hide_opts')) {
-<div class="container">
-<div class="input-field">
-
-
-%= form_for _redirect => begin
-<div>
- <div class="field">
- <div class="desc">Bahnhof / Haltestelle</div>
- <div>
-% if (stash('stationlist')) {
- %= select_field input => stash('stationlist')
-% }
-% elsif (stash('input')) {
- %= text_field 'input', class => 'station', placeholder => 'Name oder Ril100-Kürzel'
-% }
-% else {
- %= text_field 'input', class => 'station', placeholder => 'Name oder Ril100-Kürzel', autofocus => 'autofocus'
-% }
- </div>
- </div>
- <div class="field">
- %= submit_button 'Abfahrtsmonitor'
- </div>
- % if (not stash('show_intro')) {
- <div class="break"></div>
- <div class="field">
- <a class="button" href="<%= url_for('_autostop')->to_abs->scheme('https') %>">Bahnhöfe im Umfeld suchen</a>
- </div>
- % }
- <div class="break"></div>
- <div class="moresettings-header moresettings-header-collapsed button button-light">Weitere Einstellungen</div>
- <div class="moresettings moresettings-collapsed">
- <div class="field">
- <div class="desc">
- Frontend
- </div>
- <div>
- %= select_field mode => [ ['App' => 'app'], ['Infoscreen' => 'infoscreen'], ['Bahnhofstafel' => 'multi'], ['Gleisanzeiger' => 'single'] ]
- </div>
- </div>
- <div class="field">
- <div class="desc">
- Nur Züge über
- </div>
- <div>
- %= text_field 'via', placeholder => 'Bahnhof 1, Bhf2, ... (oder regulärer Ausdruck)', class => 'station'
- </div>
- </div>
- <div class="field">
- <div class="desc">
- Gleise
- </div>
- <div>
- %= text_field 'platforms', placeholder => '1, 2, 5, ...'
- </div>
- </div>
- <div class="field">
- <div class="desc">
- %= check_box 'hidelowdelay' => 1, id => 'id_hidelowdelay'
- <label for="id_hidelowdelay">
- Nur Verspätungen &gt;5 Min. anzeigen
- </label>
- </div>
- </div>
- <div class="field">
- <div class="desc">
- %= check_box 'dark' => 1, id => 'id_dark'
- <label for="id_dark">
- Dunkles Layout (experimentell)
- </label>
- </div>
- </div>
- <div class="field">
- <div class="desc">
- %= check_box 'hide_opts' => 1, id => 'id_hide_opts'
- <label for="id_hide_opts">
- Formular verstecken (für Infoscreens)
- </label>
- </div>
- </div>
- <div class="field">
- <div class="desc">
- Ankunfts- oder Abfahrtszeit anzeigen?
- </div>
- <div>
- %= select_field admode => [['Abfahrt bevorzugen' => 'deparr'], ['Nur Abfahrt' => 'dep'], ['Nur Ankunft' => 'arr']]
- </div>
- </div>
- <div class="field">
- <div class="desc">
- %= check_box 'detailed' => 1, id => 'id_detailed'
- <label for="id_detailed">
- Mehr Details (Zugnummern und Ankunftszeiten) anzeigen
- </label>
- </div>
- </div>
- <div class="field">
- <div class="desc">
- %= check_box 'rt' => 1, id => 'id_show_realtime'
- <label for="id_show_realtime">
- Echtzeitangaben statt Fahrplandaten anzeigen
- </label>
- </div>
- </div>
- <div class="field">
- <div class="desc">
- %= check_box 'no_related' => 1, id => 'id_no_related'
- <label for="id_no_related">
- Betriebliche Bahnhofstrennungen berücksichtigen (z.B. "Hbf (Fern+Regio)" vs. "Hbf (S)")
- </label>
- </div>
- </div>
- <div class="field">
- %= submit_button 'Anzeigen'
- </div>
- </div> <!-- moresettings -->
-</div>
-% end
-
-</div> <!-- input-field -->
-
-<div class="notes">
- <div class="developers-header developers-header-collapsed button button-light">API- und Entwickler-Hinweise</div>
- <div class="developers developers-collapsed">
- <ul>
- <li>Diese Seite kann gerne als iframe in eigene Infoscreens o.ä. eingebunden werden.
- Für eine kleine Ansicht (z.B. iframe in einer normalen Website) bitte das
- "App"-Frontend verwenden. Für eine große Ansicht
- (z.B. als alleinstehender Infoscreen) gibt es das "Infoscreen"-Frontend.</li>
- <li>Die Parameter <span style="font-family: monospace;">mode=json&amp;version=3</span>
- (alternativ auch <span style="font-family:
- monospace;">https://dbf.finalrewind.org/Bahnhofsname.json?version=3</span>)
- bieten ein JSON-IRIS-Interface. Die route-Elemente können zusätzlich
- die Felder "isAdditional" oder "isCancelled" enthalten, der Rest sollte
- selbsterklärend sein. Im Fehlerfall fehlt das "departures"-Element,
- stattdessen wird ein "error"-Element mit Fehlermeldung zurückgegeben.
- Bitte nur eine Anfrage pro Station und Minute
- – eine höhere Auflösung haben die Backenddaten ohnehin nicht.</li>
- <li>Mit <span style="font-family: monospace;">limit</span> kann die Anzahl der
- angezeigten / im JSON enthaltenen Abfahrten eingeschränkt werden, z.B.
- <span style="font-family: monospace;">limit=10</span> für die ersten zehn.</li>
- <li>Dieser Dienst ist Open Source-Software (Links siehe unten) und kann auch
- auf eigenen Servern installiert werden. Automatisierte Crawler, die mehrere
- Dutzend Stationen pro Minute abfragen, bitte nur auf eigenen Instanzen
- betreiben.</li>
- </ul>
- </div> <!-- developers -->
-</div> <!-- notes -->
-
-</div> <!-- container -->
-
-<div class="container">
-<div class="about">
-<a href="_about">Über DBF</a>
-<a href="_datenschutz" rel="nofollow">Datenschutz</a>
-<a href="_impressum" rel="nofollow">Impressum</a><br/>
-Version <%= stash('version') // '???' %>
-</div> <!-- about -->
-</div> <!-- container -->
-% }
-
</body>
</html>
diff --git a/templates/route_map.html.ep b/templates/route_map.html.ep
index 2f35b54..e1c4642 100644
--- a/templates/route_map.html.ep
+++ b/templates/route_map.html.ep
@@ -1,12 +1,9 @@
% if (stash('origin') and stash('destination')) {
%= include '_map_infobox'
% }
-% elsif (stash('intersection')) {
- %= include '_intersection_infobox'
-% }
<div class="container">
- <div id="map" style="height: 500px;">
+ <div id="map" style="height: 70vh;">
</div>
</div>
@@ -82,18 +79,20 @@ var marker;
</script>
<div class="container" style="margin-top: 1ex; margin-bottom: 1ex; color: #555;">
-Die eingezeichnete Route stammt aus dem HAFAS und ist im Detail oft
-fehlerbehaftet.<br/>
-Die Zugposition auf der Karte ist eine DBF-eigene Schätzung und kann erheblich
-von den tatsächlichen Gegebenheiten abweichen.
+<p>
+Die eingezeichnete Route stammt aus dem angefragten Backend und stimmt nicht
+notwendigerweise mit der Realität überein.
+Die Fahrzeugposition auf der Karte ist eine DBF-eigene Schätzung und kann
+erheblich von den tatsächlichen Gegebenheiten abweichen.
% if (stash('intersection')) {
<br/>In dieser Ansicht sind Live-Updates der Zug- und Begegnungspositionen noch
nicht implementiert.
% }
+</p>
</div>
% if (my $op = stash('operator')) {
<div class="container" style="margin-top: 1ex; margin-bottom: 1ex; color: #555;">
-Betrieb: <%= $op %>
+<p>Betrieb: <%= $op %></p>
</div>
% }
diff --git a/templates/select_backend.html.ep b/templates/select_backend.html.ep
new file mode 100644
index 0000000..c6d2a4c
--- /dev/null
+++ b/templates/select_backend.html.ep
@@ -0,0 +1,46 @@
+<div class="container">
+ <p>
+ Das Backend bestimmt die Datenquelle für Stations- und Zuginformationen.
+ Innerhalb Deutschlands ist <strong>Deutsche Bahn</strong> via IRIS-TTS eine gute Wahl für Schienenverkehr im Bahnnetz.
+ Die anderen Backends bieten sich für Fahrten im zugehörigen Verkehrsverbund (inklusive Nahverkehr) sowie im Ausland an.
+ Sofern bekannt sind unterhalb der Backend-Namen Karten verlinkt, die die ungefähre Abdeckung aufzeigen.
+ Ein Backend, welches Nah- und Fernverkehr in ganz Deutschland abdeckt, ist aktuell leider nicht verfügbar.
+ </p>
+ <p>
+ % my $prev_type = 'IRIS-TTS';
+ % for my $backend (@{$backends}) {
+ <p>
+ % if ($backend->{type} ne $prev_type) {
+ % $prev_type = $backend->{type};
+ <%= $prev_type %>:<br/>
+ % }
+ % my $class = 'button';
+ % if (param('efa')) {
+ % if ($backend->{efa} and $backend->{shortname} eq param('efa')) {
+ % $class .= ' button-active';
+ % }
+ % }
+ % elsif (param('hafas')) {
+ % if ($backend->{hafas} and $backend->{shortname} eq param('hafas')) {
+ % $class .= ' button-active';
+ % }
+ % }
+ % else {
+ % if (not ($backend->{efa} or $backend->{hafas})) {
+ % $class .= ' button-active';
+ % }
+ % }
+ <a class="<%= $class %>" href="<%= url_for(q{/})->query({ efa => $backend->{efa} ? $backend->{shortname} : q{}, hafas => $backend->{hafas} ? $backend->{shortname} : q{} }) %>"><%= $backend->{shortname} // 'IRIS-TTS' %> – <%= $backend->{name} %></a>
+ % if ($backend->{has_area}) {
+ <a href="/coverage/<%= $backend->{type} %>/<%= $backend->{shortname} %>"><%= join(q{, }, @{$backend->{regions}}) || '[Karte]' %></a>
+ % }
+ % else {
+ %= join(q{, }, @{$backend->{regions} // []})
+ % }
+ % if ($backend->{homepage}) {
+ (<a href="<%= $backend->{homepage} %>"><%= $backend->{homepage} =~ s{ ^ http s? :// (?: www[.] )? (.*?) (?: / )? $ }{$1}xr %></a>)
+ % }
+ </p>
+ % }
+ </p>
+</div>
diff --git a/templates/trainsearch.html.ep b/templates/trainsearch.html.ep
deleted file mode 100644
index 5d6d3f9..0000000
--- a/templates/trainsearch.html.ep
+++ /dev/null
@@ -1,29 +0,0 @@
-<div class="container">
- <div class="input-field">
- %= form_for _trainsearch => begin
- <div>
- <div class="field">
- <div class="desc">Zug</div>
- <div>
- %= text_field 'train1', placeholder => 'RE 1234', id => 'train1_input', autofocus => 'autofocus'
- </div>
- </div>
- <div class="field">
- %= submit_button 'Strecke zeigen'
- </div>
- <div class="break"></div>
- <div class="field">
- <div class="desc">
- Zweiter Zug (optional)
- </div>
- <div>
- %= text_field 'train2', placeholder => 'S 5678', id => 'train2_input'
- </div>
- </div>
- <div class="field">
- %= submit_button 'Begegnungen suchen (beta)'
- </div>
- </div>
- % end
- </div>
-</div>
diff --git a/templates/wagenreihung.html.ep b/templates/wagenreihung.html.ep
index 493d9c6..19c49ab 100644
--- a/templates/wagenreihung.html.ep
+++ b/templates/wagenreihung.html.ep
@@ -1,64 +1,56 @@
-% if (not $wr or $wr->errstr) {
+% if (not $wr or $wr_error) {
<div class="container">
<div class="error">
<strong>Fehler bei der Abfrage der Wagenreihung:</strong>
- <%= $wr ? $wr->errstr : $wr_error %>
+ <%= $wr_error // 'Unbekannter Fehler' %>
</div>
</div>
% }
% else {
- % my $has_multi_dest = 0;
- % my $has_multi_desc = 0;
- % if (scalar $wr->destinations > 1) {
- % $has_multi_dest = 1;
- % }
- % if (scalar $wr->train_descriptions > 1) {
- % $has_multi_desc = 1;
- % }
<div class="container">
<div style="text-align: center;">
-%= join( ' / ', $wr->origins )
- →
-%= join( ' / ', map { $_->{name} } $wr->destinations )
+ Gleis <%= $wr->platform %><br/>
</div>
- % if ($has_multi_dest) {
- <div style="text-align: center;">
- % for my $destination ($wr->destinations) {
- Nach <%= $destination->{name} %> in Abschnitt <%= join(q{}, sort @{$destination->{sections} // []}) %><br/>
- % }
- </div>
- % }
- <%= $wr->station_name %> Gleis <%= $wr->platform %><br/>
- % for my $desc ($wr->train_descriptions) {
- % if ($desc->{text}) {
- %= $desc->{text}
- % if ($has_multi_desc and length(join(q{}, sort @{$desc->{sections}}))) {
- in Abschnitt <%= join(q{}, sort @{$desc->{sections}}) %>
- % }
- <br/>
- % }
- % }
</div>
<div class="container">
<div class="wagonorder exit-<%= stash('exit_dir') // 'unknown'%>">
-% if (not $wr->has_bad_wagons) {
-% for my $section ($wr->sections) {
- <div class="section" style="
- top: <%= $section->{start_percent} %>%; bottom: <%= 100 - $section->{end_percent} %>%;">
-%= $section->{name}
- </div>
-% }
+% for my $sector ($wr->sectors) {
+ <div class="section" style="
+ top: <%= $sector->start_percent %>%; bottom: <%= 100 - $sector->end_percent %>%;">
+%= $sector->name
+ </div>
% }
-% for my $wagon ($wr->wagons) {
-%= include '_wagon', direction => $wr->direction, wagon => $wagon, type => $wr->train_type, wref => $wref, exit_dir => stash('exit_dir');
+% for my $group ($wr->groups) {
+% my $first = 1;
+% for my $wagon ($group->carriages) {
+%= include '_wagon', wr => $wr, group => $group, wagon => $wagon, first => $first, multi => (scalar $wr->destinations) - 1 + (scalar $wr->train_numbers) - 1, wref => $wref, exit_dir => stash('exit_dir'), train_no => param('number');
+% $first = 0;
+% }
% }
</div>
+ % for my $group ($wr->groups) {
+ % if ($group->description) {
+ <div style="text-align: center;">
+ %= $group->description
+ % if ($group->designation) {
+ „<%= $group->designation %>“
+ % }
+ % if (scalar $wr->groups > 1 and $group->has_sectors) {
+ in Abschnitt <%= join(q{}, sort $group->sectors) %>
+ % }
+ </div>
+ % }
+ % }
+ <div style="text-align: center;">
+ nach
+%= join( ' / ', map { $_->{name} } $wr->destinations )
+ </div>
<!-- <div>
Legende: ♿ Behindertengerechte Ausstattung / 🍴 Bistro/Restaurant / 🚪 Abteile vorhanden
</div>
-->
<p class="copyright">
- Quelle: DB Wagenreihungs-API. Angaben ohne Gewähr.
+ Quelle: DB Wagenreihungs-API (<%= stash('ts') // q{} %>). Angaben ohne Gewähr.
</p>
</div>