diff options
41 files changed, 3753 insertions, 1347 deletions
@@ -2,13 +2,11 @@ db-infoscreen - App/Infoscreen for Railway Departures in Germany --- [db-infoscreen](https://finalrewind.org/projects/db-fakedisplay/) (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-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 @@ -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.12'; -requires 'Travel::Status::DE::HAFAS', '>= 5.06'; +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 5fa0b69..7c74172 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 @@ -111,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 @@ -199,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 @@ -223,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 @@ -259,10 +259,10 @@ 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 Const-Fast-0.014 @@ -290,19 +290,19 @@ DISTRIBUTIONS perl 5.012 strict 0 warnings 0 - DateTime-1.65 - pathname: D/DR/DROLSKY/DateTime-1.65.tar.gz - provides: - DateTime 1.65 - DateTime::Duration 1.65 - DateTime::Helpers 1.65 - DateTime::Infinite 1.65 - DateTime::Infinite::Future 1.65 - DateTime::Infinite::Past 1.65 - DateTime::LeapSecond 1.65 - DateTime::PP 1.65 - DateTime::PPExtra 1.65 - DateTime::Types 1.65 + 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 @@ -312,7 +312,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 @@ -329,6 +329,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: @@ -354,15 +393,15 @@ DISTRIBUTIONS parent 0 strict 0 warnings 0 - DateTime-Locale-1.41 - pathname: D/DR/DROLSKY/DateTime-Locale-1.41.tar.gz + DateTime-Locale-1.45 + pathname: D/DR/DROLSKY/DateTime-Locale-1.45.tar.gz provides: - DateTime::Locale 1.41 - DateTime::Locale::Base 1.41 - DateTime::Locale::Catalog 1.41 - DateTime::Locale::Data 1.41 - DateTime::Locale::FromData 1.41 - DateTime::Locale::Util 1.41 + 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 @@ -380,346 +419,335 @@ DISTRIBUTIONS perl 5.008004 strict 0 warnings 0 - DateTime-TimeZone-2.62 - pathname: D/DR/DROLSKY/DateTime-TimeZone-2.62.tar.gz - provides: - DateTime::TimeZone 2.62 - DateTime::TimeZone::Africa::Abidjan 2.62 - DateTime::TimeZone::Africa::Algiers 2.62 - DateTime::TimeZone::Africa::Bissau 2.62 - DateTime::TimeZone::Africa::Cairo 2.62 - DateTime::TimeZone::Africa::Casablanca 2.62 - DateTime::TimeZone::Africa::Ceuta 2.62 - DateTime::TimeZone::Africa::El_Aaiun 2.62 - DateTime::TimeZone::Africa::Johannesburg 2.62 - DateTime::TimeZone::Africa::Juba 2.62 - DateTime::TimeZone::Africa::Khartoum 2.62 - DateTime::TimeZone::Africa::Lagos 2.62 - DateTime::TimeZone::Africa::Maputo 2.62 - DateTime::TimeZone::Africa::Monrovia 2.62 - DateTime::TimeZone::Africa::Nairobi 2.62 - DateTime::TimeZone::Africa::Ndjamena 2.62 - DateTime::TimeZone::Africa::Sao_Tome 2.62 - DateTime::TimeZone::Africa::Tripoli 2.62 - DateTime::TimeZone::Africa::Tunis 2.62 - DateTime::TimeZone::Africa::Windhoek 2.62 - DateTime::TimeZone::America::Adak 2.62 - DateTime::TimeZone::America::Anchorage 2.62 - DateTime::TimeZone::America::Araguaina 2.62 - DateTime::TimeZone::America::Argentina::Buenos_Aires 2.62 - DateTime::TimeZone::America::Argentina::Catamarca 2.62 - DateTime::TimeZone::America::Argentina::Cordoba 2.62 - DateTime::TimeZone::America::Argentina::Jujuy 2.62 - DateTime::TimeZone::America::Argentina::La_Rioja 2.62 - DateTime::TimeZone::America::Argentina::Mendoza 2.62 - DateTime::TimeZone::America::Argentina::Rio_Gallegos 2.62 - DateTime::TimeZone::America::Argentina::Salta 2.62 - DateTime::TimeZone::America::Argentina::San_Juan 2.62 - DateTime::TimeZone::America::Argentina::San_Luis 2.62 - DateTime::TimeZone::America::Argentina::Tucuman 2.62 - DateTime::TimeZone::America::Argentina::Ushuaia 2.62 - DateTime::TimeZone::America::Asuncion 2.62 - DateTime::TimeZone::America::Bahia 2.62 - DateTime::TimeZone::America::Bahia_Banderas 2.62 - DateTime::TimeZone::America::Barbados 2.62 - DateTime::TimeZone::America::Belem 2.62 - DateTime::TimeZone::America::Belize 2.62 - DateTime::TimeZone::America::Boa_Vista 2.62 - DateTime::TimeZone::America::Bogota 2.62 - DateTime::TimeZone::America::Boise 2.62 - DateTime::TimeZone::America::Cambridge_Bay 2.62 - DateTime::TimeZone::America::Campo_Grande 2.62 - DateTime::TimeZone::America::Cancun 2.62 - DateTime::TimeZone::America::Caracas 2.62 - DateTime::TimeZone::America::Cayenne 2.62 - DateTime::TimeZone::America::Chicago 2.62 - DateTime::TimeZone::America::Chihuahua 2.62 - DateTime::TimeZone::America::Ciudad_Juarez 2.62 - DateTime::TimeZone::America::Costa_Rica 2.62 - DateTime::TimeZone::America::Cuiaba 2.62 - DateTime::TimeZone::America::Danmarkshavn 2.62 - DateTime::TimeZone::America::Dawson 2.62 - DateTime::TimeZone::America::Dawson_Creek 2.62 - DateTime::TimeZone::America::Denver 2.62 - DateTime::TimeZone::America::Detroit 2.62 - DateTime::TimeZone::America::Edmonton 2.62 - DateTime::TimeZone::America::Eirunepe 2.62 - DateTime::TimeZone::America::El_Salvador 2.62 - DateTime::TimeZone::America::Fort_Nelson 2.62 - DateTime::TimeZone::America::Fortaleza 2.62 - DateTime::TimeZone::America::Glace_Bay 2.62 - DateTime::TimeZone::America::Goose_Bay 2.62 - DateTime::TimeZone::America::Grand_Turk 2.62 - DateTime::TimeZone::America::Guatemala 2.62 - DateTime::TimeZone::America::Guayaquil 2.62 - DateTime::TimeZone::America::Guyana 2.62 - DateTime::TimeZone::America::Halifax 2.62 - DateTime::TimeZone::America::Havana 2.62 - DateTime::TimeZone::America::Hermosillo 2.62 - DateTime::TimeZone::America::Indiana::Indianapolis 2.62 - DateTime::TimeZone::America::Indiana::Knox 2.62 - DateTime::TimeZone::America::Indiana::Marengo 2.62 - DateTime::TimeZone::America::Indiana::Petersburg 2.62 - DateTime::TimeZone::America::Indiana::Tell_City 2.62 - DateTime::TimeZone::America::Indiana::Vevay 2.62 - DateTime::TimeZone::America::Indiana::Vincennes 2.62 - DateTime::TimeZone::America::Indiana::Winamac 2.62 - DateTime::TimeZone::America::Inuvik 2.62 - DateTime::TimeZone::America::Iqaluit 2.62 - DateTime::TimeZone::America::Jamaica 2.62 - DateTime::TimeZone::America::Juneau 2.62 - DateTime::TimeZone::America::Kentucky::Louisville 2.62 - DateTime::TimeZone::America::Kentucky::Monticello 2.62 - DateTime::TimeZone::America::La_Paz 2.62 - DateTime::TimeZone::America::Lima 2.62 - DateTime::TimeZone::America::Los_Angeles 2.62 - DateTime::TimeZone::America::Maceio 2.62 - DateTime::TimeZone::America::Managua 2.62 - DateTime::TimeZone::America::Manaus 2.62 - DateTime::TimeZone::America::Martinique 2.62 - DateTime::TimeZone::America::Matamoros 2.62 - DateTime::TimeZone::America::Mazatlan 2.62 - DateTime::TimeZone::America::Menominee 2.62 - DateTime::TimeZone::America::Merida 2.62 - DateTime::TimeZone::America::Metlakatla 2.62 - DateTime::TimeZone::America::Mexico_City 2.62 - DateTime::TimeZone::America::Miquelon 2.62 - DateTime::TimeZone::America::Moncton 2.62 - DateTime::TimeZone::America::Monterrey 2.62 - DateTime::TimeZone::America::Montevideo 2.62 - DateTime::TimeZone::America::New_York 2.62 - DateTime::TimeZone::America::Nome 2.62 - DateTime::TimeZone::America::Noronha 2.62 - DateTime::TimeZone::America::North_Dakota::Beulah 2.62 - DateTime::TimeZone::America::North_Dakota::Center 2.62 - DateTime::TimeZone::America::North_Dakota::New_Salem 2.62 - DateTime::TimeZone::America::Nuuk 2.62 - DateTime::TimeZone::America::Ojinaga 2.62 - DateTime::TimeZone::America::Panama 2.62 - DateTime::TimeZone::America::Paramaribo 2.62 - DateTime::TimeZone::America::Phoenix 2.62 - DateTime::TimeZone::America::Port_au_Prince 2.62 - DateTime::TimeZone::America::Porto_Velho 2.62 - DateTime::TimeZone::America::Puerto_Rico 2.62 - DateTime::TimeZone::America::Punta_Arenas 2.62 - DateTime::TimeZone::America::Rankin_Inlet 2.62 - DateTime::TimeZone::America::Recife 2.62 - DateTime::TimeZone::America::Regina 2.62 - DateTime::TimeZone::America::Resolute 2.62 - DateTime::TimeZone::America::Rio_Branco 2.62 - DateTime::TimeZone::America::Santarem 2.62 - DateTime::TimeZone::America::Santiago 2.62 - DateTime::TimeZone::America::Santo_Domingo 2.62 - DateTime::TimeZone::America::Sao_Paulo 2.62 - DateTime::TimeZone::America::Scoresbysund 2.62 - DateTime::TimeZone::America::Sitka 2.62 - DateTime::TimeZone::America::St_Johns 2.62 - DateTime::TimeZone::America::Swift_Current 2.62 - DateTime::TimeZone::America::Tegucigalpa 2.62 - DateTime::TimeZone::America::Thule 2.62 - DateTime::TimeZone::America::Tijuana 2.62 - DateTime::TimeZone::America::Toronto 2.62 - DateTime::TimeZone::America::Vancouver 2.62 - DateTime::TimeZone::America::Whitehorse 2.62 - DateTime::TimeZone::America::Winnipeg 2.62 - DateTime::TimeZone::America::Yakutat 2.62 - DateTime::TimeZone::Antarctica::Casey 2.62 - DateTime::TimeZone::Antarctica::Davis 2.62 - DateTime::TimeZone::Antarctica::Macquarie 2.62 - DateTime::TimeZone::Antarctica::Mawson 2.62 - DateTime::TimeZone::Antarctica::Palmer 2.62 - DateTime::TimeZone::Antarctica::Rothera 2.62 - DateTime::TimeZone::Antarctica::Troll 2.62 - DateTime::TimeZone::Antarctica::Vostok 2.62 - DateTime::TimeZone::Asia::Almaty 2.62 - DateTime::TimeZone::Asia::Amman 2.62 - DateTime::TimeZone::Asia::Anadyr 2.62 - DateTime::TimeZone::Asia::Aqtau 2.62 - DateTime::TimeZone::Asia::Aqtobe 2.62 - DateTime::TimeZone::Asia::Ashgabat 2.62 - DateTime::TimeZone::Asia::Atyrau 2.62 - DateTime::TimeZone::Asia::Baghdad 2.62 - DateTime::TimeZone::Asia::Baku 2.62 - DateTime::TimeZone::Asia::Bangkok 2.62 - DateTime::TimeZone::Asia::Barnaul 2.62 - DateTime::TimeZone::Asia::Beirut 2.62 - DateTime::TimeZone::Asia::Bishkek 2.62 - DateTime::TimeZone::Asia::Chita 2.62 - DateTime::TimeZone::Asia::Choibalsan 2.62 - DateTime::TimeZone::Asia::Colombo 2.62 - DateTime::TimeZone::Asia::Damascus 2.62 - DateTime::TimeZone::Asia::Dhaka 2.62 - DateTime::TimeZone::Asia::Dili 2.62 - DateTime::TimeZone::Asia::Dubai 2.62 - DateTime::TimeZone::Asia::Dushanbe 2.62 - DateTime::TimeZone::Asia::Famagusta 2.62 - DateTime::TimeZone::Asia::Gaza 2.62 - DateTime::TimeZone::Asia::Hebron 2.62 - DateTime::TimeZone::Asia::Ho_Chi_Minh 2.62 - DateTime::TimeZone::Asia::Hong_Kong 2.62 - DateTime::TimeZone::Asia::Hovd 2.62 - DateTime::TimeZone::Asia::Irkutsk 2.62 - DateTime::TimeZone::Asia::Jakarta 2.62 - DateTime::TimeZone::Asia::Jayapura 2.62 - DateTime::TimeZone::Asia::Jerusalem 2.62 - DateTime::TimeZone::Asia::Kabul 2.62 - DateTime::TimeZone::Asia::Kamchatka 2.62 - DateTime::TimeZone::Asia::Karachi 2.62 - DateTime::TimeZone::Asia::Kathmandu 2.62 - DateTime::TimeZone::Asia::Khandyga 2.62 - DateTime::TimeZone::Asia::Kolkata 2.62 - DateTime::TimeZone::Asia::Krasnoyarsk 2.62 - DateTime::TimeZone::Asia::Kuching 2.62 - DateTime::TimeZone::Asia::Macau 2.62 - DateTime::TimeZone::Asia::Magadan 2.62 - DateTime::TimeZone::Asia::Makassar 2.62 - DateTime::TimeZone::Asia::Manila 2.62 - DateTime::TimeZone::Asia::Nicosia 2.62 - DateTime::TimeZone::Asia::Novokuznetsk 2.62 - DateTime::TimeZone::Asia::Novosibirsk 2.62 - DateTime::TimeZone::Asia::Omsk 2.62 - DateTime::TimeZone::Asia::Oral 2.62 - DateTime::TimeZone::Asia::Pontianak 2.62 - DateTime::TimeZone::Asia::Pyongyang 2.62 - DateTime::TimeZone::Asia::Qatar 2.62 - DateTime::TimeZone::Asia::Qostanay 2.62 - DateTime::TimeZone::Asia::Qyzylorda 2.62 - DateTime::TimeZone::Asia::Riyadh 2.62 - DateTime::TimeZone::Asia::Sakhalin 2.62 - DateTime::TimeZone::Asia::Samarkand 2.62 - DateTime::TimeZone::Asia::Seoul 2.62 - DateTime::TimeZone::Asia::Shanghai 2.62 - DateTime::TimeZone::Asia::Singapore 2.62 - DateTime::TimeZone::Asia::Srednekolymsk 2.62 - DateTime::TimeZone::Asia::Taipei 2.62 - DateTime::TimeZone::Asia::Tashkent 2.62 - DateTime::TimeZone::Asia::Tbilisi 2.62 - DateTime::TimeZone::Asia::Tehran 2.62 - DateTime::TimeZone::Asia::Thimphu 2.62 - DateTime::TimeZone::Asia::Tokyo 2.62 - DateTime::TimeZone::Asia::Tomsk 2.62 - DateTime::TimeZone::Asia::Ulaanbaatar 2.62 - DateTime::TimeZone::Asia::Urumqi 2.62 - DateTime::TimeZone::Asia::Ust_Nera 2.62 - DateTime::TimeZone::Asia::Vladivostok 2.62 - DateTime::TimeZone::Asia::Yakutsk 2.62 - DateTime::TimeZone::Asia::Yangon 2.62 - DateTime::TimeZone::Asia::Yekaterinburg 2.62 - DateTime::TimeZone::Asia::Yerevan 2.62 - DateTime::TimeZone::Atlantic::Azores 2.62 - DateTime::TimeZone::Atlantic::Bermuda 2.62 - DateTime::TimeZone::Atlantic::Canary 2.62 - DateTime::TimeZone::Atlantic::Cape_Verde 2.62 - DateTime::TimeZone::Atlantic::Faroe 2.62 - DateTime::TimeZone::Atlantic::Madeira 2.62 - DateTime::TimeZone::Atlantic::South_Georgia 2.62 - DateTime::TimeZone::Atlantic::Stanley 2.62 - DateTime::TimeZone::Australia::Adelaide 2.62 - DateTime::TimeZone::Australia::Brisbane 2.62 - DateTime::TimeZone::Australia::Broken_Hill 2.62 - DateTime::TimeZone::Australia::Darwin 2.62 - DateTime::TimeZone::Australia::Eucla 2.62 - DateTime::TimeZone::Australia::Hobart 2.62 - DateTime::TimeZone::Australia::Lindeman 2.62 - DateTime::TimeZone::Australia::Lord_Howe 2.62 - DateTime::TimeZone::Australia::Melbourne 2.62 - DateTime::TimeZone::Australia::Perth 2.62 - DateTime::TimeZone::Australia::Sydney 2.62 - DateTime::TimeZone::CET 2.62 - DateTime::TimeZone::CST6CDT 2.62 - DateTime::TimeZone::Catalog 2.62 - DateTime::TimeZone::EET 2.62 - DateTime::TimeZone::EST 2.62 - DateTime::TimeZone::EST5EDT 2.62 - DateTime::TimeZone::Europe::Andorra 2.62 - DateTime::TimeZone::Europe::Astrakhan 2.62 - DateTime::TimeZone::Europe::Athens 2.62 - DateTime::TimeZone::Europe::Belgrade 2.62 - DateTime::TimeZone::Europe::Berlin 2.62 - DateTime::TimeZone::Europe::Brussels 2.62 - DateTime::TimeZone::Europe::Bucharest 2.62 - DateTime::TimeZone::Europe::Budapest 2.62 - DateTime::TimeZone::Europe::Chisinau 2.62 - DateTime::TimeZone::Europe::Dublin 2.62 - DateTime::TimeZone::Europe::Gibraltar 2.62 - DateTime::TimeZone::Europe::Helsinki 2.62 - DateTime::TimeZone::Europe::Istanbul 2.62 - DateTime::TimeZone::Europe::Kaliningrad 2.62 - DateTime::TimeZone::Europe::Kirov 2.62 - DateTime::TimeZone::Europe::Kyiv 2.62 - DateTime::TimeZone::Europe::Lisbon 2.62 - DateTime::TimeZone::Europe::London 2.62 - DateTime::TimeZone::Europe::Madrid 2.62 - DateTime::TimeZone::Europe::Malta 2.62 - DateTime::TimeZone::Europe::Minsk 2.62 - DateTime::TimeZone::Europe::Moscow 2.62 - DateTime::TimeZone::Europe::Paris 2.62 - DateTime::TimeZone::Europe::Prague 2.62 - DateTime::TimeZone::Europe::Riga 2.62 - DateTime::TimeZone::Europe::Rome 2.62 - DateTime::TimeZone::Europe::Samara 2.62 - DateTime::TimeZone::Europe::Saratov 2.62 - DateTime::TimeZone::Europe::Simferopol 2.62 - DateTime::TimeZone::Europe::Sofia 2.62 - DateTime::TimeZone::Europe::Tallinn 2.62 - DateTime::TimeZone::Europe::Tirane 2.62 - DateTime::TimeZone::Europe::Ulyanovsk 2.62 - DateTime::TimeZone::Europe::Vienna 2.62 - DateTime::TimeZone::Europe::Vilnius 2.62 - DateTime::TimeZone::Europe::Volgograd 2.62 - DateTime::TimeZone::Europe::Warsaw 2.62 - DateTime::TimeZone::Europe::Zurich 2.62 - DateTime::TimeZone::Floating 2.62 - DateTime::TimeZone::HST 2.62 - DateTime::TimeZone::Indian::Chagos 2.62 - DateTime::TimeZone::Indian::Maldives 2.62 - DateTime::TimeZone::Indian::Mauritius 2.62 - DateTime::TimeZone::Local 2.62 - DateTime::TimeZone::Local::Android 2.62 - DateTime::TimeZone::Local::Unix 2.62 - DateTime::TimeZone::Local::VMS 2.62 - DateTime::TimeZone::MET 2.62 - DateTime::TimeZone::MST 2.62 - DateTime::TimeZone::MST7MDT 2.62 - DateTime::TimeZone::OffsetOnly 2.62 - DateTime::TimeZone::OlsonDB 2.62 - DateTime::TimeZone::OlsonDB::Change 2.62 - DateTime::TimeZone::OlsonDB::Observance 2.62 - DateTime::TimeZone::OlsonDB::Rule 2.62 - DateTime::TimeZone::OlsonDB::Zone 2.62 - DateTime::TimeZone::PST8PDT 2.62 - DateTime::TimeZone::Pacific::Apia 2.62 - DateTime::TimeZone::Pacific::Auckland 2.62 - DateTime::TimeZone::Pacific::Bougainville 2.62 - DateTime::TimeZone::Pacific::Chatham 2.62 - DateTime::TimeZone::Pacific::Easter 2.62 - DateTime::TimeZone::Pacific::Efate 2.62 - DateTime::TimeZone::Pacific::Fakaofo 2.62 - DateTime::TimeZone::Pacific::Fiji 2.62 - DateTime::TimeZone::Pacific::Galapagos 2.62 - DateTime::TimeZone::Pacific::Gambier 2.62 - DateTime::TimeZone::Pacific::Guadalcanal 2.62 - DateTime::TimeZone::Pacific::Guam 2.62 - DateTime::TimeZone::Pacific::Honolulu 2.62 - DateTime::TimeZone::Pacific::Kanton 2.62 - DateTime::TimeZone::Pacific::Kiritimati 2.62 - DateTime::TimeZone::Pacific::Kosrae 2.62 - DateTime::TimeZone::Pacific::Kwajalein 2.62 - DateTime::TimeZone::Pacific::Marquesas 2.62 - DateTime::TimeZone::Pacific::Nauru 2.62 - DateTime::TimeZone::Pacific::Niue 2.62 - DateTime::TimeZone::Pacific::Norfolk 2.62 - DateTime::TimeZone::Pacific::Noumea 2.62 - DateTime::TimeZone::Pacific::Pago_Pago 2.62 - DateTime::TimeZone::Pacific::Palau 2.62 - DateTime::TimeZone::Pacific::Pitcairn 2.62 - DateTime::TimeZone::Pacific::Port_Moresby 2.62 - DateTime::TimeZone::Pacific::Rarotonga 2.62 - DateTime::TimeZone::Pacific::Tahiti 2.62 - DateTime::TimeZone::Pacific::Tarawa 2.62 - DateTime::TimeZone::Pacific::Tongatapu 2.62 - DateTime::TimeZone::UTC 2.62 - DateTime::TimeZone::WET 2.62 + 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 @@ -811,32 +839,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 @@ -845,19 +876,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 @@ -998,16 +1028,16 @@ DISTRIBUTIONS parent 0 perl 5.008001 strictures 2.000000 - HTML-Parser-3.82 - pathname: O/OA/OALDERS/HTML-Parser-3.82.tar.gz + HTML-Parser-3.83 + pathname: O/OA/OALDERS/HTML-Parser-3.83.tar.gz provides: - HTML::Entities 3.82 - HTML::Filter 3.82 - HTML::HeadParser 3.82 - HTML::LinkExtor 3.82 - HTML::Parser 3.82 - HTML::PullParser 3.82 - HTML::TokeParser 3.82 + 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 @@ -1052,19 +1082,19 @@ DISTRIBUTIONS Time::Zone 0 perl 5.006002 strict 0 - HTTP-Message-6.45 - pathname: O/OA/OALDERS/HTTP-Message-6.45.tar.gz - provides: - HTTP::Config 6.45 - HTTP::Headers 6.45 - HTTP::Headers::Auth 6.45 - HTTP::Headers::ETag 6.45 - HTTP::Headers::Util 6.45 - HTTP::Message 6.45 - HTTP::Request 6.45 - HTTP::Request::Common 6.45 - HTTP::Response 6.45 - HTTP::Status 6.45 + 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 @@ -1125,24 +1155,37 @@ DISTRIBUTIONS Exporter 5.57 ExtUtils::MakeMaker 0 perl 5.008 - IO-Socket-SSL-2.085 - pathname: S/SU/SULLR/IO-Socket-SSL-2.085.tar.gz + IO-Socket-SSL-2.094 + pathname: S/SU/SULLR/IO-Socket-SSL-2.094.tar.gz provides: - IO::Socket::SSL 2.085 + IO::Socket::SSL 2.094 IO::Socket::SSL::Intercept 2.056 - IO::Socket::SSL::OCSP_Cache 2.085 - IO::Socket::SSL::OCSP_Resolver 2.085 + IO::Socket::SSL::OCSP_Cache 2.094 + IO::Socket::SSL::OCSP_Resolver 2.094 IO::Socket::SSL::PublicSuffix undef - IO::Socket::SSL::SSL_Context 2.085 - IO::Socket::SSL::SSL_HANDLE 2.085 - IO::Socket::SSL::Session_Cache 2.085 - IO::Socket::SSL::Trace 2.085 + IO::Socket::SSL::SSL_Context 2.094 + IO::Socket::SSL::SSL_HANDLE 2.094 + IO::Socket::SSL::Session_Cache 2.094 + IO::Socket::SSL::Trace 2.094 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: @@ -1234,6 +1277,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: @@ -1286,10 +1338,10 @@ DISTRIBUTIONS Text::ParseWords 0 perl 5.006001 version 0.87 - Module-Build-Tiny-0.047 - pathname: L/LE/LEONT/Module-Build-Tiny-0.047.tar.gz + Module-Build-Tiny-0.052 + pathname: L/LE/LEONT/Module-Build-Tiny-0.052.tar.gz provides: - Module::Build::Tiny 0.047 + Module::Build::Tiny 0.052 requirements: CPAN::Meta 0 DynaLoader 0 @@ -1322,24 +1374,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.36 - pathname: S/SR/SRI/Mojolicious-9.36.tar.gz + ExtUtils::MakeMaker 0 + perl 5.006000 + Mojolicious-9.40 + pathname: S/SR/SRI/Mojolicious-9.40.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 @@ -1401,7 +1451,7 @@ DISTRIBUTIONS Mojo::UserAgent::Transactor undef Mojo::Util undef Mojo::WebSocket undef - Mojolicious 9.36 + Mojolicious 9.40 Mojolicious::Command undef Mojolicious::Command::Author::cpanify undef Mojolicious::Command::Author::generate undef @@ -1450,10 +1500,10 @@ DISTRIBUTIONS IO::Socket::IP 0.37 Sub::Util 1.41 perl 5.016 - Mozilla-CA-20240313 - pathname: L/LW/LWP/Mozilla-CA-20240313.tar.gz + Mozilla-CA-20250602 + pathname: L/LW/LWP/Mozilla-CA-20250602.tar.gz provides: - Mozilla::CA 20240313 + Mozilla::CA 20250602 requirements: ExtUtils::MakeMaker 0 Net-HTTP-6.23 @@ -1533,6 +1583,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: @@ -1551,11 +1620,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.148 + pathname: D/DA/DAGOLDEN/Path-Tiny-0.148.tar.gz provides: - Path::Tiny 0.144 - Path::Tiny::Error 0.144 + Path::Tiny 0.148 + Path::Tiny::Error 0.148 requirements: Carp 0 Cwd 0 @@ -1578,10 +1647,10 @@ DISTRIBUTIONS strict 0 warnings 0 warnings::register 0 - PkgConfig-0.25026 - pathname: P/PL/PLICEASE/PkgConfig-0.25026.tar.gz + PkgConfig-0.26026 + pathname: P/PL/PLICEASE/PkgConfig-0.26026.tar.gz provides: - PkgConfig 0.25026 + PkgConfig 0.26026 requirements: ExtUtils::MakeMaker 6.56 Test::More 0.94 @@ -1594,52 +1663,53 @@ 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.50 + pathname: D/DR/DROLSKY/Specio-0.50.tar.gz + provides: + Specio 0.50 + Specio::Coercion 0.50 + Specio::Constraint::AnyCan 0.50 + Specio::Constraint::AnyDoes 0.50 + Specio::Constraint::AnyIsa 0.50 + Specio::Constraint::Enum 0.50 + Specio::Constraint::Intersection 0.50 + Specio::Constraint::ObjectCan 0.50 + Specio::Constraint::ObjectDoes 0.50 + Specio::Constraint::ObjectIsa 0.50 + Specio::Constraint::Parameterizable 0.50 + Specio::Constraint::Parameterized 0.50 + Specio::Constraint::Role::CanType 0.50 + Specio::Constraint::Role::DoesType 0.50 + Specio::Constraint::Role::Interface 0.50 + Specio::Constraint::Role::IsaType 0.50 + Specio::Constraint::Simple 0.50 + Specio::Constraint::Structurable 0.50 + Specio::Constraint::Structured 0.50 + Specio::Constraint::Union 0.50 + Specio::Declare 0.50 + Specio::DeclaredAt 0.50 + Specio::Exception 0.50 + Specio::Exporter 0.50 + Specio::Helpers 0.50 + Specio::Library::Builtins 0.50 + Specio::Library::Numeric 0.50 + Specio::Library::Perl 0.50 + Specio::Library::String 0.50 + Specio::Library::Structured 0.50 + Specio::Library::Structured::Dict 0.50 + Specio::Library::Structured::Map 0.50 + Specio::Library::Structured::Tuple 0.50 + Specio::OO 0.50 + Specio::PartialDump 0.50 + Specio::Registry 0.50 + Specio::Role::Inlinable 0.50 + Specio::Subs 0.50 + Specio::TypeChecks 0.50 + Test::Specio 0.50 requirements: B 0 Carp 0 + Clone 0 Devel::StackTrace 0 Eval::Closure 0 Exporter 0 @@ -1651,7 +1721,6 @@ DISTRIBUTIONS Role::Tiny 1.003003 Role::Tiny::With 0 Scalar::Util 0 - Storable 0 Sub::Quote 0 Test::Fatal 0 Test::More 0.96 @@ -1684,13 +1753,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: @@ -1712,17 +1774,16 @@ DISTRIBUTIONS 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: @@ -1759,12 +1820,12 @@ DISTRIBUTIONS Test::Builder::Tester 1.02 Test::More 0.62 perl 5.008 - Text-CSV-2.04 - pathname: I/IS/ISHIGAKI/Text-CSV-2.04.tar.gz + Text-CSV-2.06 + pathname: I/IS/ISHIGAKI/Text-CSV-2.06.tar.gz provides: - Text::CSV 2.04 - Text::CSV::ErrorDiag 2.04 - Text::CSV_PP 2.04 + Text::CSV 2.06 + Text::CSV::ErrorDiag 2.06 + Text::CSV_PP 2.06 requirements: ExtUtils::MakeMaker 0 IO::Handle 0 @@ -1823,37 +1884,44 @@ DISTRIBUTIONS TimeDate 1.21 requirements: ExtUtils::MakeMaker 0 - Travel-Status-DE-DBWagenreihung-0.12 - pathname: D/DE/DERF/Travel-Status-DE-DBWagenreihung-0.12.tar.gz + Travel-Status-DE-DBRIS-0.11 + pathname: D/DE/DERF/Travel-Status-DE-DBRIS-0.11.tar.gz provides: - Travel::Status::DE::DBWagenreihung 0.12 - Travel::Status::DE::DBWagenreihung::Section 0.12 - Travel::Status::DE::DBWagenreihung::Wagon 0.12 + Travel::Status::DE::DBRIS 0.11 + Travel::Status::DE::DBRIS::Formation 0.11 + Travel::Status::DE::DBRIS::Formation::Carriage 0.11 + Travel::Status::DE::DBRIS::Formation::Group 0.11 + Travel::Status::DE::DBRIS::Formation::Sector 0.11 + Travel::Status::DE::DBRIS::Journey 0.11 + Travel::Status::DE::DBRIS::JourneyAtStop 0.11 + Travel::Status::DE::DBRIS::Location 0.11 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-6.02 - pathname: D/DE/DERF/Travel-Status-DE-DeutscheBahn-6.02.tar.gz - provides: - Travel::Status::DE::DeutscheBahn 6.02 - Travel::Status::DE::HAFAS 6.02 - Travel::Status::DE::HAFAS::Journey 6.02 - Travel::Status::DE::HAFAS::Location 6.02 - Travel::Status::DE::HAFAS::Message 6.02 - Travel::Status::DE::HAFAS::Polyline 6.02 - Travel::Status::DE::HAFAS::Product 6.02 - Travel::Status::DE::HAFAS::Stop 6.02 - Travel::Status::DE::HAFAS::StopFinder 6.02 + Travel-Status-DE-HAFAS-6.20 + pathname: D/DE/DERF/Travel-Status-DE-HAFAS-6.20.tar.gz + provides: + Travel::Status::DE::HAFAS 6.20 + Travel::Status::DE::HAFAS::Journey 6.20 + Travel::Status::DE::HAFAS::Location 6.20 + Travel::Status::DE::HAFAS::Message 6.20 + Travel::Status::DE::HAFAS::Polyline 6.20 + Travel::Status::DE::HAFAS::Product 6.20 + Travel::Status::DE::HAFAS::Services 6.20 + Travel::Status::DE::HAFAS::Stop 6.20 + Travel::Status::DE::HAFAS::StopFinder 6.20 requirements: Carp 0 Class::Accessor 0.16 @@ -1871,12 +1939,12 @@ DISTRIBUTIONS Test::More 0 Test::Pod 0 perl v5.14.0 - Travel-Status-DE-IRIS-1.96 - pathname: D/DE/DERF/Travel-Status-DE-IRIS-1.96.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.96 - Travel::Status::DE::IRIS::Result 1.96 - Travel::Status::DE::IRIS::Stations 1.96 + 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 @@ -1903,10 +1971,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.12 + pathname: D/DE/DERF/Travel-Status-DE-VRR-3.12.tar.gz + provides: + Travel::Status::DE::EFA 3.12 + Travel::Status::DE::EFA::Departure 3.12 + Travel::Status::DE::EFA::Info 3.12 + Travel::Status::DE::EFA::Line 3.12 + Travel::Status::DE::EFA::Services 3.12 + Travel::Status::DE::EFA::Stop 3.12 + Travel::Status::DE::EFA::Trip 3.12 + Travel::Status::DE::VRR 3.12 + 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.02 + pathname: D/DE/DERF/Travel-Status-MOTIS-0.02.tar.gz + provides: + Travel::Status::MOTIS 0.02 + Travel::Status::MOTIS::Polyline 0.02 + Travel::Status::MOTIS::Services 0.02 + Travel::Status::MOTIS::Stop 0.02 + Travel::Status::MOTIS::Stopover 0.02 + Travel::Status::MOTIS::Trip 0.02 + Travel::Status::MOTIS::TripAtStopover 0.02 + 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 @@ -1925,56 +2044,63 @@ DISTRIBUTIONS requirements: ExtUtils::MakeMaker 0 common::sense 0 - URI-5.28 - pathname: O/OA/OALDERS/URI-5.28.tar.gz - provides: - URI 5.28 - URI::Escape 5.28 - URI::Heuristic 5.28 - URI::IRI 5.28 - URI::QueryParam 5.28 - URI::Split 5.28 - URI::URL 5.28 - URI::WithBase 5.28 - URI::data 5.28 - URI::file 5.28 - URI::file::Base 5.28 - URI::file::FAT 5.28 - URI::file::Mac 5.28 - URI::file::OS2 5.28 - URI::file::QNX 5.28 - URI::file::Unix 5.28 - URI::file::Win32 5.28 - URI::ftp 5.28 - URI::geo 5.28 - URI::gopher 5.28 - URI::http 5.28 - URI::https 5.28 - URI::icap 5.28 - URI::icaps 5.28 - URI::ldap 5.28 - URI::ldapi 5.28 - URI::ldaps 5.28 - URI::mailto 5.28 - URI::mms 5.28 - URI::news 5.28 - URI::nntp 5.28 - URI::nntps 5.28 - URI::pop 5.28 - URI::rlogin 5.28 - URI::rsync 5.28 - URI::rtsp 5.28 - URI::rtspu 5.28 - URI::sftp 5.28 - URI::sip 5.28 - URI::sips 5.28 - URI::snews 5.28 - URI::ssh 5.28 - URI::telnet 5.28 - URI::tn3270 5.28 - URI::urn 5.28 - URI::urn::isbn 5.28 - URI::urn::oid 5.28 + 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 +2108,7 @@ DISTRIBUTIONS Encode 0 Exporter 5.57 ExtUtils::MakeMaker 0 + MIME::Base32 0 MIME::Base64 2 Net::Domain 0 Scalar::Util 0 @@ -2172,32 +2299,32 @@ DISTRIBUTIONS XSLoader 0 lib 0 perl 5.008001 - libwww-perl-6.77 - pathname: O/OA/OALDERS/libwww-perl-6.77.tar.gz - provides: - LWP 6.77 - LWP::Authen::Basic 6.77 - LWP::Authen::Digest 6.77 - LWP::Authen::Ntlm 6.77 - LWP::ConnCache 6.77 - LWP::Debug 6.77 - LWP::Debug::TraceHTTP 6.77 - LWP::DebugFile 6.77 - LWP::MemberMixin 6.77 - LWP::Protocol 6.77 - LWP::Protocol::cpan 6.77 - LWP::Protocol::data 6.77 - LWP::Protocol::file 6.77 - LWP::Protocol::ftp 6.77 - LWP::Protocol::gopher 6.77 - LWP::Protocol::http 6.77 - LWP::Protocol::loopback 6.77 - LWP::Protocol::mailto 6.77 - LWP::Protocol::nntp 6.77 - LWP::Protocol::nogo 6.77 - LWP::RobotUA 6.77 - LWP::Simple 6.77 - LWP::UserAgent 6.77 + libwww-perl-6.78 + pathname: O/OA/OALDERS/libwww-perl-6.78.tar.gz + provides: + LWP 6.78 + LWP::Authen::Basic 6.78 + LWP::Authen::Digest 6.78 + LWP::Authen::Ntlm 6.78 + LWP::ConnCache 6.78 + LWP::Debug 6.78 + LWP::Debug::TraceHTTP 6.78 + LWP::DebugFile 6.78 + LWP::MemberMixin 6.78 + LWP::Protocol 6.78 + LWP::Protocol::cpan 6.78 + LWP::Protocol::data 6.78 + LWP::Protocol::file 6.78 + LWP::Protocol::ftp 6.78 + LWP::Protocol::gopher 6.78 + LWP::Protocol::http 6.78 + LWP::Protocol::loopback 6.78 + LWP::Protocol::mailto 6.78 + LWP::Protocol::nntp 6.78 + LWP::Protocol::nogo 6.78 + LWP::RobotUA 6.78 + LWP::Simple 6.78 + LWP::UserAgent 6.78 requirements: Digest::MD5 0 Encode 2.12 @@ -2247,15 +2374,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 c33a703..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; @@ -92,6 +94,34 @@ sub startup { ); $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->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}, + ); + } + ); + + $self->helper( efa => sub { my ($self) = @_; state $efa = DBInfoscreen::Helper::EFA->new( @@ -178,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; } ); @@ -187,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'; } @@ -255,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'); @@ -264,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'); @@ -274,20 +348,25 @@ sub startup { $r->get('/dyn/:av/autocomplete.js')->to('stationboard#autocomplete'); - $r->get('/_wr/:train/:departure')->to('wagenreihung#wagenreihung'); + $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( '/z/:train/*station' => 'train_at_station' ) - ->to('stationboard#station_train_details'); - $r->get( '/z/:train' => 'train' )->to('stationboard#train_details'); + $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 bced612..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. @@ -65,10 +68,10 @@ sub estimate_train_positions { my $now = $opt{now}; - my $from_dt = $opt{from}->dep // $opt{from}->arr; - my $to_dt = $opt{to}->arr // $opt{to}->dep; - my $from_name = $opt{from}->loc->name; - my $to_name = $opt{to}->loc->name; + my $from_dt = $opt{from}{dep} // $opt{from}{arr}; + my $to_dt = $opt{to}{arr} // $opt{to}{dep}; + my $from_name = $opt{from}{name}; + my $to_name = $opt{to}{name}; my $route = $opt{route}; my $polyline = $opt{polyline}; @@ -142,16 +145,14 @@ sub estimate_train_positions { ); for my $ratio (@completion_ratios) { my $lat - = $opt{from}->loc->lat - + ( $opt{to}->loc->lat - $opt{from}->loc->lat ) * $ratio; + = $opt{from}{lat} + ( $opt{to}{lat} - $opt{from}{lat} ) * $ratio; my $lon - = $opt{from}->loc->lon - + ( $opt{to}->loc->lon - $opt{from}->loc->lon ) * $ratio; + = $opt{from}{lon} + ( $opt{to}{lon} - $opt{from}{lon} ) * $ratio; push( @train_positions, [ $lat, $lon ] ); } return @train_positions; } - return [ $opt{to}->loc->lat, $opt{to}->loc->lon ]; + return [ $opt{to}{lat}, $opt{to}{lon} ]; } # Input: @@ -162,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} @@ -179,10 +182,10 @@ sub estimate_train_positions2 { for my $i ( 1 .. $#route ) { if ( not $next_stop - and ( $route[$i]->arr // $route[$i]->dep ) - and ( $route[ $i - 1 ]->dep // $route[ $i - 1 ]->arr ) - and $now > ( $route[ $i - 1 ]->dep // $route[ $i - 1 ]->arr ) - and $now < ( $route[$i]->arr // $route[$i]->dep ) ) + and ( $route[$i]{arr} // $route[$i]{dep} ) + and ( $route[ $i - 1 ]{dep} // $route[ $i - 1 ]{arr} ) + and $now > ( $route[ $i - 1 ]{dep} // $route[ $i - 1 ]{arr} ) + and $now < ( $route[$i]{arr} // $route[$i]{dep} ) ) { # HAFAS does not provide delays for past stops @@ -207,15 +210,15 @@ sub estimate_train_positions2 { and $now <= ( $route[ $i - 1 ]{dep} // $route[ $i - 1 ]{arr} ) ) { @train_positions - = ( [ $route[ $i - 1 ]->loc->lat, $route[ $i - 1 ]->loc->lon ] ); + = ( [ $route[ $i - 1 ]{lat}, $route[ $i - 1 ]{lon} ] ); $next_stop = { type => 'present', station => $route[ $i - 1 ], }; } $stop_distance_sum += $distance->distance_metal( - $route[ $i - 1 ]->loc->lat, $route[ $i - 1 ]->loc->lon, - $route[$i]->loc->lat, $route[$i]->loc->lon + $route[ $i - 1 ]{lat}, $route[ $i - 1 ]{lon}, + $route[$i]{lat}, $route[$i]{lon} ) / 1000; } @@ -224,7 +227,7 @@ sub estimate_train_positions2 { } if ( @route and not $next_stop ) { - @train_positions = ( [ $route[-1]->loc->lat, $route[-1]->loc->lon ] ); + @train_positions = ( [ $route[-1]{lat}, $route[-1]{lon} ] ); $next_stop = { type => 'present', station => $route[-1] @@ -241,18 +244,23 @@ sub estimate_train_positions2 { }; } +# input: [{ +# name, platform, +# arr, arr_cancelled, arr_delay, +# dep, dep_cancelled, dep_delay +# }] sub route_to_ajax { my (@stopovers) = @_; my @route_entries; for my $stop (@stopovers) { - my @stop_entries = ( $stop->loc->name ); + my @stop_entries = ( $stop->{name} ); my $platform; - if ( my $arr = $stop->arr and not $stop->arr_cancelled ) { - my $delay = $stop->arr_delay // 0; - $platform = $stop->platform; + if ( my $arr = $stop->{arr} and not $stop->{arr_cancelled} ) { + my $delay = $stop->{arr_delay} // 0; + $platform = $stop->{platform}; push( @stop_entries, $arr->epoch, $delay ); } @@ -260,9 +268,9 @@ sub route_to_ajax { push( @stop_entries, q{}, q{} ); } - if ( my $dep = $stop->dep and not $stop->dep_cancelled ) { - my $delay = $stop->dep_delay // 0; - $platform //= $stop->platform // q{}; + if ( my $dep = $stop->{dep} and not $stop->{dep_cancelled} ) { + my $delay = $stop->{dep_delay} // 0; + $platform //= $stop->{platform} // q{}; push( @stop_entries, $dep->epoch, $delay, $platform ); } @@ -310,6 +318,494 @@ 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'); @@ -321,7 +817,17 @@ sub route { $self->render_later; - my $service = 'DB'; + 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) ) @@ -341,7 +847,6 @@ sub route { my @station_coordinates; my @markers; - my $next_stop; my $now = DateTime->now( time_zone => 'Europe/Berlin' ); @@ -351,8 +856,20 @@ 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, ); @@ -413,18 +930,32 @@ 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]->loc->name, ts => ( $journey->route )[0]->dep, @@ -434,10 +965,10 @@ sub route { 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], @@ -467,17 +998,284 @@ sub route { )->wait; } -sub ajax_route { +sub ajax_route_efa { my ($self) = @_; + my $backend = $self->param('efa'); + my $trip_id = $self->stash('tripid'); + + 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->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; +} + +sub ajax_route_dbris { + my ($self) = @_; + my $trip_id = $self->stash('tripid'); + + $self->dbris->get_polyline_p( id => $trip_id )->then( + sub { + 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 => $_->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}/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, + }, + destination => { + 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}, + platform_type => q{}, + ); + } + )->catch( + sub { + my ($err) = @_; + $self->render( + '_error', + error => $err, + ); + } + )->wait; +} + +sub ajax_route_motis { + my ($self) = @_; + + my $service = $self->param('motis') // 'transitous'; my $trip_id = $self->stash('tripid'); - my $line_no = $self->stash('lineno'); - my $hafas = $self->param('hafas'); + + $self->motis->get_polyline_p( + service => $service, + id => $trip_id, + )->then( + sub { + my ($trip) = @_; + + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + + 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; - my $service = 'DB'; + 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'); + + my $service = 'ÖBB'; if ( $hafas and $hafas ne '1' and Travel::Status::DE::HAFAS::get_service($hafas) ) @@ -499,17 +1297,44 @@ sub ajax_route { my @polyline = $journey->polyline; 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, ); $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}/${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, @@ -535,4 +1360,30 @@ sub ajax_route { )->wait; } +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( + 'coverage_map', + title => "Abdeckung $service", + hide_opts => 1, + with_map => 1, + coverage => encode_json($coverage), + ); +} + 1; 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 9657bc0..3e07f90 100644 --- a/lib/DBInfoscreen/Controller/Stationboard.pm +++ b/lib/DBInfoscreen/Controller/Stationboard.pm @@ -16,7 +16,9 @@ use List::MoreUtils qw(); use Mojo::JSON qw(decode_json encode_json); use Mojo::Promise; use Mojo::UserAgent; -use Travel::Status::DE::DBWagenreihung; +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; @@ -44,13 +46,32 @@ sub class_to_product { } 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 = 'DB'; + my $service = 'ÖBB'; if ( $hafas ne '1' and Travel::Status::DE::HAFAS::get_service($hafas) ) { $service = $hafas; @@ -59,7 +80,7 @@ sub handle_no_results { 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) = @_; @@ -231,10 +252,17 @@ sub result_has_train_type { sub result_has_via { my ( $result, $via ) = @_; - my @route - = $result->can('route_post') ? $result->route_post : map { $_->loc->name } - $result->route; + 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) { @@ -343,8 +371,69 @@ 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 = 'DB'; + my $service = 'ÖBB'; if ( $opt{hafas} ne '1' and Travel::Status::DE::HAFAS::get_service( $opt{hafas} ) ) { @@ -360,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, ); } @@ -407,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 $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, ); @@ -469,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; } @@ -502,6 +601,14 @@ 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, @@ -526,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 ); @@ -546,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; } @@ -607,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; } @@ -733,25 +851,88 @@ sub render_train { my @requests = ( $wagonorder_req, $occupancy_req, $stationinfo_req, $route_req ); - if ( $departure->{wr_link} ) { - $self->wagonorder->get_p( $result->train_no, $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 { - my ($wr_json) = @_; + my ( $wr_json, $wr_param ) = @_; eval { my $wr - = Travel::Status::DE::DBWagenreihung->new( - from_json => $wr_json ); + = 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 { $_->{short} } - grep { $_->{short} } $wr->train_descriptions ); + 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( @@ -821,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; @@ -937,27 +1121,36 @@ sub render_train { # 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, - 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; @@ -973,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, @@ -1059,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, }; @@ -1080,21 +1275,444 @@ 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 $dbris = $self->param('dbris'); + my $efa = $self->param('efa'); my $hafas = $self->param('hafas'); # TODO error handling @@ -1103,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, @@ -1134,7 +1757,10 @@ sub train_details { } my $service = 'DB'; - if ( $hafas ne '1' and Travel::Status::DE::HAFAS::get_service($hafas) ) { + if ( $hafas + and $hafas ne '1' + and Travel::Status::DE::HAFAS::get_service($hafas) ) + { $opt{service} = $hafas; } @@ -1191,19 +1817,19 @@ sub train_details { my $prod = $self->class_to_product($hafas_obj)->{ $product->class } // q{}; - if ( $prod eq 'ice' or $prod eq 'ic_ec' ) { + if ( $prod =~ m{ ^ ice? | inter-?cit }ix ) { $linetype = 'fern'; } - elsif ( $prod eq 's' ) { + elsif ( $prod =~ m{ s-bahn | urban | rapid }ix ) { $linetype = 'sbahn'; } - elsif ( $prod eq 'bus' ) { + elsif ( $prod =~ m{ bus }ix ) { $linetype = 'bus'; } - elsif ( $prod eq 'u' ) { + elsif ( $prod =~ m{ metro | u-bahn | subway }ix ) { $linetype = 'ubahn'; } - elsif ( $prod eq 'tram' ) { + elsif ( $prod =~ m{ tram }ix ) { $linetype = 'tram'; } } @@ -1300,40 +1926,308 @@ sub train_details { $res->{details} = [@him_details]; } - $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, - 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} }; @@ -1344,7 +2238,7 @@ 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; @@ -1386,16 +2280,32 @@ 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; } } @@ -1442,19 +2352,19 @@ sub handle_result { } elsif ( $result->can('class') ) { my $prod = $class_to_product->{ $result->class } // q{}; - if ( $prod eq 'ice' or $prod eq 'ic_ec' ) { + if ( $prod =~ m{ ^ ice? | inter-?cit }ix ) { $linetype = 'fern'; } - elsif ( $prod eq 's' ) { + elsif ( $prod =~ m{ s-bahn | urban | rapid }ix ) { $linetype = 'sbahn'; } - elsif ( $prod eq 'bus' ) { + elsif ( $prod =~ m{ bus }ix ) { $linetype = 'bus'; } - elsif ( $prod eq 'u' ) { + elsif ( $prod =~ m{ metro | u-bahn | subway }ix ) { $linetype = 'ubahn'; } - elsif ( $prod eq 'tram' ) { + elsif ( $prod =~ m{ tram }ix ) { $linetype = 'tram'; } } @@ -1511,6 +2421,9 @@ sub handle_result { ); return; } + elsif ( $apiver eq 'raw' ) { + push( @departures, $result ); + } else { # apiver == 3 if ( $result->isa('Travel::Status::DE::IRIS::Result') ) { my ( $delay_arr, $delay_dep, $sched_arr, $sched_dep ); @@ -1659,11 +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 @@ -1680,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, } ); } @@ -1716,22 +2632,26 @@ sub handle_result { 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' + 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 { $_->loc->name } $result->route ], - wr_link => $result->sched_datetime - ? $result->sched_datetime->strftime('%Y%m%d%H%M') - : undef, + wr_dt => $result->sched_datetime, + eva => $result->station_uic, } ); } @@ -1782,24 +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') ) { + if ( not $hafas ) { if ( $data->{station_eva} >= 8100000 and $data->{station_eva} < 8200000 ) { $params->param( hafas => 'ÖBB' ); } - $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'; + 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( @@ -1815,6 +2733,7 @@ sub handle_result { 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 => ( @@ -1837,16 +2756,17 @@ sub handle_result { sub stations_by_coordinates { my $self = shift; - my $lon = $self->param('lon'); - my $lat = $self->param('lat'); - my $hafas = $self->param('hafas'); + 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 = 'DB'; + my $service = 'ÖBB'; if ( $hafas and $hafas ne '1' and Travel::Status::DE::HAFAS::get_service($hafas) ) @@ -1856,6 +2776,46 @@ sub stations_by_coordinates { $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], @@ -1873,7 +2833,7 @@ 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, @@ -1915,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) = @_; @@ -1963,11 +3018,18 @@ sub redirect_to_station { $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 1708285..b9f0ee3 100644 --- a/lib/DBInfoscreen/Controller/Wagenreihung.pm +++ b/lib/DBInfoscreen/Controller/Wagenreihung.pm @@ -10,39 +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 handle_wagenreihung_error { - my ( $self, $train_no, $err ) = @_; + my ( $self, $train, $err ) = @_; $self->render( 'wagenreihung', - title => "Zug $train_no", + title => $train, wr_error => $err, - train_no => $train_no, wr => undef, wref => undef, hide_opts => 1, + status => 500, ); } 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 $@ ); @@ -50,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}{}; @@ -71,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 @@ -100,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+$} @@ -161,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; } 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 2a7416e..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 ) = @_; diff --git a/lib/DBInfoscreen/Helper/HAFAS.pm b/lib/DBInfoscreen/Helper/HAFAS.pm index cdb84f0..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,6 +30,18 @@ sub new { } +sub get_coverage { + my ( $self, $service ) = @_; + + my $service_definition = Travel::Status::DE::HAFAS::get_service($service); + + if ( not $service_definition ) { + return {}; + } + + return $service_definition->{coverage}{area} // {}; +} + sub get_route_p { my ( $self, %opt ) = @_; @@ -37,16 +50,23 @@ sub get_route_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}, + 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} ) { @@ -58,12 +78,13 @@ sub get_route_p { } $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 => $self->{user_agent}->request_timeout(10) + user_agent => $agent->request_timeout(10) )->then( sub { my ($hafas) = @_; @@ -88,13 +109,14 @@ sub get_route_p { } return Travel::Status::DE::HAFAS->new_p( + service => $opt{service} // 'ÖBB', journey => { 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) ); } ); @@ -253,9 +275,16 @@ sub get_polyline_p { my $trip_id = $opt{id}; my $line = $opt{line}; - my $service = $opt{service}; + 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 => { @@ -265,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 5cdee40..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 { @@ -25,21 +26,50 @@ sub new { } sub get_p { - my ( $self, $train_no, $api_ts ) = @_; + my ( $self, %opt ) = @_; - my $url - = "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}"; + my %param; - my $cache = $self->{realtime_cache}; + if ( $opt{param} ) { + %param = %{ $opt{param} }; + delete $param{e}; + } + 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 + ); + } + + 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; - if ( my $content = $cache->thaw($url) ) { + if ( my $content = $self->{main_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 ); + } + + if ( my $content = $self->{realtime_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)" + ); + } + return $promise->resolve( $content, \%param ); } $self->{user_agent}->request_timeout(10)->get_p( $url => $self->{header} ) @@ -57,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/public/static/css/dark.min.css b/public/static/css/dark.min.css index 1a63845..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.config,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 .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:#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;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 .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: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 .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-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/light.min.css b/public/static/css/light.min.css index 676c894..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.config,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 .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:#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;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 .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: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 .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-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 d8ff2b9..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/v96/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/v96/fonts/MaterialIcons-Regular.woff2) format('woff2'), - url(/static/v96/fonts/MaterialIcons-Regular.woff) format('woff'), - url(/static/v96/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 d7d1e3b..e861169 100644 --- a/public/static/js/collapse.js +++ b/public/static/js/collapse.js @@ -96,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'); @@ -113,6 +114,12 @@ function dbf_reg_handlers() { if (param.get('detailed')) { suffix += '&detailed=1'; } + if (param.get('dbris') && param.get('dbris') != '0') { + suffix += '&dbris=' + param.get('dbris') + '&highlight=' + trainElem.data('station'); + } + 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'); } @@ -124,6 +131,10 @@ function dbf_reg_handlers() { } 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 da4ac90..f977bbd 100644 --- a/public/static/js/dbf.min.js +++ b/public/static/js/dbf.min.js @@ -1 +1 @@ -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(i in l)t+="<li>"+l[i]+"</li>";$(".moreinfo .mfooter").append("Meldungen: <ul>"+t+"</ul>")}var o="";if(""!=d.data("routeprev"))for(var i in n)o+="<li>"+n[i]+"</li>";if(o+="<li><strong>"+document.title+"</strong></li>",""!=d.data("routenext"))for(var i in r)o+="<li>"+r[i]+"</li>";$(".moreinfo .mfooter").append('Fahrtverlauf: <ul class="mroute">'+o+"</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"),o=new URLSearchParams(window.location.search),e=(e.preventDefault(),"?");o.get("detailed")&&(e+="&detailed=1"),o.get("hafas")&&"0"!=o.get("hafas")&&(e+="&hafas="+o.get("hafas")+"&highlight="+a.data("station")),o.get("past")&&(e+="&past=1"),(o.get("rt")||o.get("show_realtime"))&&(e+="&rt=1"),o.get("hafas")&&"0"!=o.get("hafas")?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 fa2d6f1..69bb607 100644 --- a/public/static/js/geostop.js +++ b/public/static/js/geostop.js @@ -39,10 +39,13 @@ $(function() { const eva = candidate.eva, name = candidate.name, distance = candidate.distance.toFixed(1), + efa = candidate.efa, hafas = candidate.hafas; const stationlink = $(document.createElement('a')); - if (hafas) { + if (efa) { + stationlink.attr('href', eva + '?efa=' + efa); + } else if (hafas) { stationlink.attr('href', eva + '?hafas=' + hafas); } else { stationlink.attr('href', eva); @@ -55,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); @@ -66,7 +69,7 @@ $(function() { const processLocation = function(loc) { const param = new URLSearchParams(window.location.search); - $.post('/_geolocation', {lon: loc.coords.longitude, lat: loc.coords.latitude, hafas: param.get('hafas')}, processResult).fail(function(jqXHR, textStatus, errorThrown) { + $.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 41e18e0..5998966 100644 --- a/public/static/js/geostop.min.js +++ b/public/static/js/geostop.min.js @@ -1 +1 @@ -$(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),t=t.hafas,o=$(document.createElement("a")),n=(t?o.attr("href",n+"?hafas="+t):o.attr("href",n),o.text(a+" "),$(document.createElement("div"))),a=(n.attr("class","distance"),n.text(r),$(document.createElement("i")));a.attr("class","material-icons"),a.text(t?"directions":"train"),o.append(a),o.append(n),$("div.candidatelist").append(o)})}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,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))}); +$(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 0389323..fcaac86 100644 --- a/public/static/js/map-refresh.js +++ b/public/static/js/map-refresh.js @@ -69,7 +69,14 @@ function dbf_anim_fine() { function dbf_map_reload() { const param = new URLSearchParams(window.location.search); - $.get('/_ajax_mapinfo/' + j_reqid + '?hafas=' + param.get('hafas'), function(data) { + + 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 b988098..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 r=1;r<=60;r++){var f=r/60;j_frame.push([e+(_-e)*f,i+(t-i)*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);$.get("/_ajax_mapinfo/"+j_reqid+"?hafas="+a.get("hafas"),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/v95 b/public/static/v109 index 945c9b4..945c9b4 120000 --- a/public/static/v95 +++ b/public/static/v109 diff --git a/public/static/v96 b/public/static/v110 index 945c9b4..945c9b4 120000 --- a/public/static/v96 +++ b/public/static/v110 diff --git a/sass/app.scss b/sass/app.scss index f24ac9c..75074bd 100644 --- a/sass/app.scss +++ b/sass/app.scss @@ -38,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; } @@ -158,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; } @@ -195,7 +218,7 @@ div.content { font-weight: bold; } - .uic78::before { + .uic78:before { content: "-"; } @@ -213,7 +236,7 @@ div.content { color: $fg3; } - .uiccheck::before { + .uiccheck:before { content: "-"; } } @@ -268,6 +291,9 @@ div.app { &.cancelled { background-color: $cancelled-bg-color; + .time { + color: $fg !important; + } } &.past { @@ -366,6 +392,12 @@ div.app { } } + .load { + color: $fg; + font-weight: normal; + margin-right: 0.5em; + } + .platform { background-color: transparent; font-size: 3em; @@ -396,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; @@ -542,6 +584,14 @@ div.app { a { color: $fg; } + + .otherno { + color: $fg2; + } + + .meta { + color: $fg1; + } } .departure { @@ -955,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; @@ -970,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 c0e8d2b..78b61b0 100644 --- a/sass/dark.scss +++ b/sass/dark.scss @@ -32,9 +32,11 @@ $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; diff --git a/sass/light.scss b/sass/light.scss index 1dee6a9..60981b1 100644 --- a/sass/light.scss +++ b/sass/light.scss @@ -32,9 +32,11 @@ $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; 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/_map_infobox.html.ep b/templates/_map_infobox.html.ep index 7372802..16625f5 100644 --- a/templates/_map_infobox.html.ep +++ b/templates/_map_infobox.html.ep @@ -14,7 +14,7 @@ data-poly="<%= stash('ajax_polyline') %>" % if (my $next = stash('next_stop')) { <div class="nextstop"> % if ($next->{type} eq 'present' and $next->{station}{dep} and $next->{station}{arr}) { - Aufenthalt in <strong><%= $next->{station}->loc->name %></strong> + Aufenthalt in <strong><%= $next->{station}{name} %></strong> % if ($next->{station}{platform}) { an Gleis <strong><%= $next->{station}{platform} %></strong> % } @@ -24,7 +24,7 @@ data-poly="<%= stash('ajax_polyline') %>" % } % } % elsif ($next->{type} eq 'present' and $next->{station}{dep}) { - Abfahrt in <strong><%= $next->{station}->loc->name %></strong> + Abfahrt in <strong><%= $next->{station}{name} %></strong> % if ($next->{station}{platform}) { von Gleis <strong><%= $next->{station}{platform} %></strong> % } @@ -52,7 +52,7 @@ data-poly="<%= stash('ajax_polyline') %>" % } % elsif ($next->{type} eq 'next' and $next->{station}{arr}) { Nächster Halt: - <strong><%= $next->{station}->loc->name %></strong> + <strong><%= $next->{station}{name} %></strong> um <strong><%= $next->{station}{arr}->strftime('%H:%M') %></strong> % if ($next->{station}{arr_delay}) { %= sprintf('(%+d)', $next->{station}{arr_delay}) @@ -63,7 +63,7 @@ data-poly="<%= stash('ajax_polyline') %>" % } % elsif ($next->{type} eq 'next') { Nächster Halt: - <strong><%= $next->{station}->loc->name %></strong> + <strong><%= $next->{station}{name} %></strong> % if ($next->{station}{platform}) { auf Gleis <strong><%= $next->{station}{platform} %></strong> % } diff --git a/templates/_train_details.html.ep b/templates/_train_details.html.ep index 709a1ac..2c18da2 100644 --- a/templates/_train_details.html.ep +++ b/templates/_train_details.html.ep @@ -2,8 +2,8 @@ <div> % if ($departure->{train_no} or $departure->{train_line}) { <span class="train-line <%= $linetype %>"><%= $departure->{train_type} %> - - <%= $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> @@ -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} @@ -118,49 +118,46 @@ </div> <!-- dataline --> % if (my $wr = $departure->{wr}) { <div class="wagonorder-preview"> -% my @wagons = $wr->wagons; -% my $direction = $wr->direction ? $wr->direction == 100 ? '→' : '←' : q{}; -% if ($departure->{direction}) { -% $direction = $departure->{direction} eq 'l' ? '◀' : '▶'; -% if (($departure->{direction} eq 'l' ? 0 : 100) != $wr->direction) { -% @wagons = reverse @wagons; -% } +% 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{}; % } - <a href="/_wr/<%= $departure->{train_no} %>/<%= $departure->{wr_link} %>?e=<%= $departure->{direction} // '' %>"> - %= $direction -% my $gi; -% for my $wagon (@wagons) { -% if (not ($wagon->is_locomotive or $wagon->is_powercar)) { -% if (defined $gi and $gi != $wagon->group_index) { - • -% } -% if ($wagon->is_closed) { - X -% } -% else { -%= $wagon->number || ($wagon->type =~ m{AB} ? '½' : $wagon->type =~ m{A} ? '1.' : $wagon->type =~ m{B} ? '2.' : $wagon->type ) -% } -% } -% $gi = $wagon->group_index; +% elsif ($departure->{wr_direction} and $departure->{wr_direction} =~ m{r}) { +% $left = q{}; +% $right = '▶'; % } - %= $direction + <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} =~ s{#}{%23}gr %>/<%= $departure->{train_line} || 0 %>?from=<%= stash('station_name') %>&hafas=<%= param('hafas') // q{} %>"><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} =~ s{#}{%23}gr %>/<%= $departure->{train_line} || 0 %>?hafas=<%= param('hafas') // q{} %>"><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> <%= $departure->{wr_text} || '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> % } -% if ($departure->{train_type} and $departure->{train_no}) { +% 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}}) { @@ -257,7 +254,7 @@ </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 % } @@ -285,7 +282,9 @@ <%= $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> @@ -328,7 +327,9 @@ <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> @@ -348,7 +349,7 @@ </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 % } @@ -376,7 +377,9 @@ <%= $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> diff --git a/templates/_wagon.html.ep b/templates/_wagon.html.ep index 2f5a0df..dccecc0 100644 --- a/templates/_wagon.html.ep +++ b/templates/_wagon.html.ep @@ -1,6 +1,6 @@ % 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) { @@ -9,34 +9,28 @@ % if ($wagon->is_closed) { % $extra_class .= ' closed'; % } -% if ($wagon->train_no ne $train_no) { +% 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) { @@ -44,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 { @@ -55,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 { @@ -67,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) { @@ -77,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 { @@ -86,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 b5af92b..3bf8295 100644 --- a/templates/about.html.ep +++ b/templates/about.html.ep @@ -1,7 +1,8 @@ <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 – 2024 <a href="https://finalrewind.org">derf</a>. @@ -9,37 +10,28 @@ Fehlermeldungen bitte via <a href="<%= $issue_url %>">Issue Tracker</a>. % } - </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. + 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> 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> + <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> Außerdeutsche Fahrten, Nahverkehr, Details, Karten: HAFAS via <a href="https://finalrewind.org/projects/Travel-Status-DE-DeutscheBahn/">Travel::Status::DE::HAFAS</a> + <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>Wagenreihung: <a href="https://finalrewind.org/projects/Travel-Status-DE-DBWagenreihung/">Travel::Status::DE::DBWagenreihung</a> - <strong>v<%= $Travel::Status::DE::DBWagenreihung::VERSION %></strong></li> - <li>Zugauslastung Regionalverkehr: VRR EFA via <a href="https://github.com/derf/eva-to-efa-gw">eva-to-efa-gw</a></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> - Unterstützte HAFAS-Instanzen („hafas=…“): - <ul> - % for my $service (Travel::Status::DE::HAFAS::get_services()) { - <li><%= $service->{shortname} %> (<%= $service->{name} %>)</li> - % } - </ul> - </p> - <p> - Verwendete Open Data-Ressourcen: + 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, @@ -49,6 +41,12 @@ © 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> % if (-e 'templates/imprint.html.ep') { diff --git a/templates/app.html.ep b/templates/app.html.ep index 50f8a5a..8b52c61 100644 --- a/templates/app.html.ep +++ b/templates/app.html.ep @@ -30,7 +30,7 @@ % $route_str .= $stop . ($via_cur < $via_max ? ' - ' : q{}); % } <li -% if (param('hafas')) { +% 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{}) %>" @@ -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} @@ -100,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 { @@ -140,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 @@ -151,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 17bb2bb..80fd34f 100644 --- a/templates/landingpage.html.ep +++ b/templates/landingpage.html.ep @@ -12,8 +12,9 @@ % } % else { <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 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. @@ -21,10 +22,10 @@ </p> % } <p class="geolink"> -<a class="button" href="<%= url_for('_autostop')->to_abs->scheme('https')->query({hafas => param('hafas')}) %>">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 1578298..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 = 'v96'; # 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' % } @@ -117,43 +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 'Abfahrtstafel' </div> % if (stash('input')) { <div class="geolink"> - <a class="button" href="<%= url_for('_autostop')->to_abs->scheme('https')->query({hafas => param('hafas')}) %>">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 @@ -170,17 +163,9 @@ Bitte eine Station aus der Liste auswählen</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"> - <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> @@ -194,7 +179,7 @@ Bitte eine Station aus der Liste auswählen</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' @@ -270,21 +255,22 @@ Bitte eine Station aus der Liste auswählen</div> (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>Zugdetails können optional für spezifische Abfahrtsdaten im + <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 Zugfahrt.</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 + 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>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> @@ -302,7 +288,7 @@ Bitte eine Station aus der Liste auswählen</div> <div class="container"> <div class="config"> Farbschema: -<a onClick="javascript:setTheme('light')">light</a> +<a onClick="javascript:setTheme('light')">hell</a> · <a onClick="javascript:setTheme('dark')">dunkel</a> · @@ -315,7 +301,11 @@ Farbschema: · <a onClick="javascript:setLang('default')">system language</a> --> -</div> +</div> <!-- config --> +</div> <!-- container --> +% } +% if (not stash('hide_footer')) { +<div class="container"> <div class="about"> <a href="_about">DBF</a> v<%= stash('version') // '???' %> · diff --git a/templates/layouts/legacy.html.ep b/templates/layouts/legacy.html.ep index 43a3cc5..e7e59ec 100644 --- a/templates/layouts/legacy.html.ep +++ b/templates/layouts/legacy.html.ep @@ -17,7 +17,7 @@ <meta http-equiv="refresh" content="<%= $self->stash('refresh_interval') %>"/> % } - % my $av = 'v96'; # asset version + % 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" @@ -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 447960a..e1c4642 100644 --- a/templates/route_map.html.ep +++ b/templates/route_map.html.ep @@ -3,7 +3,7 @@ % } <div class="container"> - <div id="map" style="height: 500px;"> + <div id="map" style="height: 70vh;"> </div> </div> @@ -79,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/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> |