diff options
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> @@ -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 \ @@ -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) @@ -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{(}{(}g; - $body =~ s{)}{)}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} %>&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') %>&dbris=<%= param('dbris') %>&efa=<%= param('efa') // q{} %>&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') %>&efa=<%= param('efa') // q{} %>&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} %>&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 // '' %>&s=<%= $wagon->section %>&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 // '' %>&s=<%= $wagon->section %>&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&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')) . '&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')) . '&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')) . '&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: '© <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&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&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 >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&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> |