summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/perl.yml2
-rw-r--r--README.md20
-rw-r--r--cpanfile9
-rw-r--r--cpanfile.snapshot1780
-rw-r--r--examples/travelynx.conf28
-rwxr-xr-xlib/Travelynx.pm1471
-rw-r--r--lib/Travelynx/Command/database.pm1396
-rw-r--r--lib/Travelynx/Command/dumpconfig.pm1
-rw-r--r--lib/Travelynx/Command/dumpstops.pm9
-rw-r--r--lib/Travelynx/Command/influxdb.pm101
-rw-r--r--lib/Travelynx/Command/integritycheck.pm201
-rw-r--r--lib/Travelynx/Command/maintenance.pm16
-rw-r--r--lib/Travelynx/Command/traewelling.pm38
-rw-r--r--lib/Travelynx/Command/work.pm834
-rw-r--r--lib/Travelynx/Controller/Account.pm296
-rwxr-xr-xlib/Travelynx/Controller/Api.pm67
-rw-r--r--lib/Travelynx/Controller/Passengerrights.pm7
-rwxr-xr-xlib/Travelynx/Controller/Profile.pm141
-rw-r--r--lib/Travelynx/Controller/Static.pm6
-rw-r--r--lib/Travelynx/Controller/Traewelling.pm2
-rwxr-xr-xlib/Travelynx/Controller/Traveling.pm1197
-rw-r--r--lib/Travelynx/Helper/DBDB.pm105
-rw-r--r--lib/Travelynx/Helper/DBRIS.pm146
-rw-r--r--lib/Travelynx/Helper/EFA.pm105
-rw-r--r--lib/Travelynx/Helper/HAFAS.pm150
-rw-r--r--lib/Travelynx/Helper/IRIS.pm12
-rw-r--r--lib/Travelynx/Helper/MOTIS.pm161
-rw-r--r--lib/Travelynx/Helper/Sendmail.pm2
-rw-r--r--lib/Travelynx/Helper/Traewelling.pm20
-rw-r--r--lib/Travelynx/Model/InTransit.pm824
-rwxr-xr-xlib/Travelynx/Model/Journeys.pm353
-rw-r--r--lib/Travelynx/Model/Stations.pm404
-rw-r--r--lib/Travelynx/Model/Traewelling.pm1
-rw-r--r--lib/Travelynx/Model/Users.pm58
-rw-r--r--public/service-worker.js23
-rw-r--r--public/static/css/dark.min.css2
-rw-r--r--public/static/css/light.min.css2
-rw-r--r--public/static/css/material-icons.css8
-rw-r--r--public/static/js/geolocation.js46
-rw-r--r--public/static/js/geolocation.min.js2
-rw-r--r--public/static/js/travelynx-actions.js44
-rw-r--r--public/static/js/travelynx-actions.min.js2
-rw-r--r--public/static/manifest.json12
l---------public/static/v96 (renamed from public/static/v70)0
l---------public/static/v97 (renamed from public/static/v71)0
-rw-r--r--sass/src/common/local.scss53
-rwxr-xr-xshare/ice_names.json254
-rw-r--r--t/11-journey-stats.t4
-rw-r--r--t/12-journey-edit.t90
-rw-r--r--t/22-transit-visibility.t41
-rw-r--r--t/23-journey-visibility.t17
-rw-r--r--t/24-past-visibility.t17
-rw-r--r--t/r-negative-delay.t2
-rw-r--r--templates/_backend_line.html.ep25
-rw-r--r--templates/_checked_in.html.ep205
-rw-r--r--templates/_checked_out.html.ep2
-rw-r--r--templates/_connections_hafas.html.ep13
-rw-r--r--templates/_departures_dbris.html.ep55
-rw-r--r--templates/_departures_efa.html.ep57
-rw-r--r--templates/_departures_hafas.html.ep8
-rw-r--r--templates/_departures_motis.html.ep54
-rw-r--r--templates/_format_train.html.ep6
-rw-r--r--templates/_history_trains.html.ep15
-rw-r--r--templates/_map.html.ep20
-rw-r--r--templates/_public_status_card.html.ep164
-rw-r--r--templates/_show_load_icons.html.ep11
-rw-r--r--templates/_timeline_link.html.ep2
-rw-r--r--templates/_wagons.html.ep19
-rw-r--r--templates/about.html.ep30
-rw-r--r--templates/account.html.ep18
-rw-r--r--templates/add_intransit.html.ep93
-rw-r--r--templates/add_journey.html.ep20
-rw-r--r--templates/api_documentation.html.ep15
-rw-r--r--templates/bad_gateway.html.ep27
-rw-r--r--templates/changelog.html.ep253
-rw-r--r--templates/departures.html.ep105
-rw-r--r--templates/disambiguation.html.ep2
-rw-r--r--templates/exception.html.ep11
-rw-r--r--templates/gateway_timeout.html.ep27
-rw-r--r--templates/history_by_month.html.ep6
-rw-r--r--templates/history_map.html.ep4
-rw-r--r--templates/journey.html.ep115
-rw-r--r--templates/landingpage.html.ep66
-rw-r--r--templates/layouts/default.html.ep4
-rw-r--r--templates/legend.html.ep4
-rw-r--r--templates/login.html.ep5
-rw-r--r--templates/passengerrights.html.ep69
-rw-r--r--templates/profile.html.ep2
-rw-r--r--templates/register.html.ep5
-rw-r--r--templates/select_backend.html.ep85
-rw-r--r--templates/traewelling.html.ep51
-rw-r--r--templates/use_external_links.html.ep82
-rw-r--r--templates/use_history.html.ep6
-rw-r--r--templates/user_status.html.ep2
-rwxr-xr-xupdate.sh2
95 files changed, 9279 insertions, 3068 deletions
diff --git a/.github/workflows/perl.yml b/.github/workflows/perl.yml
index 2e64b35..12a38d0 100644
--- a/.github/workflows/perl.yml
+++ b/.github/workflows/perl.yml
@@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
perl-version:
- - '5.20'
+ - '5.30'
- 'latest'
- 'threaded'
diff --git a/README.md b/README.md
index 17b7778..c9ba909 100644
--- a/README.md
+++ b/README.md
@@ -2,12 +2,15 @@ travelynx - Railway Travel Logger
---
[travelynx](https://finalrewind.org/projects/travelynx/) allows checking into
-and out of individual trains, thus providing a log of your railway journeys
-annotated with real-time delays and service messages. It supports german
-railways and trains exposed by the Deutsche Bahn [IRIS
+individual public transit vehicles (e.g. buses, ferries, trams, trains) across
+most of Germany, Switzerland, Austria, Luxembourg, Ireland, and parts of the
+USA. Thus, it provides a log of your railway journeys annotated with real-time
+delays and service messages, if available. It supports german railways and
+trains exposed by the Deutsche Bahn [IRIS
Interface](https://finalrewind.org/projects/Travel-Status-DE-IRIS/) as well as
-local transit and some trains outside of germany exposed by the Deutsche Bahn
-[HAFAS Interface](https://finalrewind.org/projects/Travel-Status-DE-DeutscheBahn/).
+regional and local transit exposed by supported [HAFAS
+Instances](https://finalrewind.org/projects/Travel-Status-DE-HAFAS/). Support
+for EFA instances and bahn.de is under way.
You can use the public instance on [travelynx.de](https://travelynx.de) or
host your own. See the Installation and Setup notes below.
@@ -79,6 +82,12 @@ Please open an issue on <https://github.com/derf/travelynx/issues> or send a
mail to derf+travelynx@finalrewind.org if there is anything missing or
ambiguous in this setup manual.
+Note that Deutsche Bahn have put parts of their API behind an IP reputation
+filter. In general, checkins with the bahn.de backend will only be possible if
+travelynx is accessing it from a residential (non-server) IP range. See the
+dbris bahn.de proxy / proxies setting in `example/travelynx.conf` for
+workarounds.
+
Updating
---
@@ -126,6 +135,7 @@ privacy policy. For the sake of this readme, we assume that you are using the
The travelynx container does not contain a mail server, so it needs a
separate SMTP server to send mail. It does not receive mail.
* create local/imprint.html.ep and enter imprint as well as privacy policy data.
+* create local/terms-of-service.html.ep and enter your terms of service.
* Configure your web server to reverse-provy requests to the travelynx
instance. See `examples/nginx-site` for an nginx config.
diff --git a/cpanfile b/cpanfile
index f81887c..04083c7 100644
--- a/cpanfile
+++ b/cpanfile
@@ -6,7 +6,10 @@ requires 'DateTime::Format::Strptime';
requires 'Email::Sender::Simple';
requires 'GIS::Distance';
requires 'GIS::Distance::Fast';
+requires 'IO::Socket::Socks', '>= 0.64';
+requires 'IO::Socket::SSL', '>= 2.009';
requires 'List::UtilsBy';
+requires 'Math::Polygon';
requires 'MIME::Entity';
requires 'Mojolicious';
requires 'Mojolicious::Plugin::Authentication';
@@ -14,8 +17,10 @@ requires 'Mojolicious::Plugin::OAuth2';
requires 'Mojo::Pg';
requires 'Text::CSV';
requires 'Text::Markdown';
-requires 'Travel::Status::DE::DBWagenreihung', '0.12';
-requires 'Travel::Status::DE::HAFAS', '>= 5.03';
+requires 'Travel::Status::DE::EFA', '>= 3.13';
+requires 'Travel::Status::MOTIS', '>= 0.01';
+requires 'Travel::Status::DE::DBRIS', '>= 0.10';
+requires 'Travel::Status::DE::HAFAS', '>= 6.20';
requires 'Travel::Status::DE::IRIS';
requires 'UUID::Tiny';
requires 'JSON';
diff --git a/cpanfile.snapshot b/cpanfile.snapshot
index 2318de5..392254a 100644
--- a/cpanfile.snapshot
+++ b/cpanfile.snapshot
@@ -7,88 +7,88 @@ DISTRIBUTIONS
Algorithm::Diff::_impl 1.201
requirements:
ExtUtils::MakeMaker 0
- 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
@@ -118,10 +118,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
@@ -226,10 +226,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
@@ -250,10 +250,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
@@ -315,10 +315,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
Clone-Choose-0.010
@@ -329,6 +329,17 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 0
Storable 0
perl 5.008001
+ Clone-PP-1.08
+ pathname: N/NE/NEILB/Clone-PP-1.08.tar.gz
+ provides:
+ Clone::PP 1.08
+ requirements:
+ Exporter 0
+ ExtUtils::MakeMaker 0
+ perl 5.006
+ strict 0
+ vars 0
+ warnings 0
Const-Fast-0.014
pathname: L/LE/LEONT/Const-Fast-0.014.tar.gz
provides:
@@ -382,8 +393,8 @@ DISTRIBUTIONS
Test::More 0.88
Time::HiRes 0
version 0
- DBI-1.643
- pathname: T/TI/TIMB/DBI-1.643.tar.gz
+ DBI-1.647
+ pathname: H/HM/HMBRAND/DBI-1.647.tgz
provides:
Bundle::DBI 12.008696
DBD::DBM 0.08
@@ -439,7 +450,7 @@ DISTRIBUTIONS
DBD::Sponge::dr 12.010003
DBD::Sponge::st 12.010003
DBDI 12.015129
- DBI 1.643
+ DBI 1.647
DBI::Const::GetInfo::ANSI 2.008697
DBI::Const::GetInfo::ODBC 2.011374
DBI::Const::GetInfoReturn 2.008697
@@ -479,7 +490,7 @@ DISTRIBUTIONS
DBI::SQL::Nano::Table_ 1.015544
DBI::Util::CacheMemory 0.010315
DBI::Util::_accessor 0.009479
- DBI::common 1.643
+ DBI::common 1.647
requirements:
ExtUtils::MakeMaker 6.48
Test::Simple 0.90
@@ -496,19 +507,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
@@ -518,7 +529,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
@@ -535,6 +546,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:
@@ -560,15 +610,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
@@ -586,346 +636,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
@@ -1130,32 +1169,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
@@ -1164,19 +1206,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
@@ -1317,16 +1358,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
@@ -1371,19 +1412,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
@@ -1453,24 +1494,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:
@@ -1562,39 +1616,48 @@ DISTRIBUTIONS
requirements:
Exporter 5.57
Module::Build 0.4004
- MIME-tools-5.514
- pathname: D/DS/DSKOLL/MIME-tools-5.514.tar.gz
- provides:
- MIME::Body 5.514
- MIME::Body::File 5.514
- MIME::Body::InCore 5.514
- MIME::Body::Scalar 5.514
- MIME::Decoder 5.514
- MIME::Decoder::Base64 5.514
- MIME::Decoder::BinHex 5.514
- MIME::Decoder::Binary 5.514
- MIME::Decoder::Gzip64 5.514
- MIME::Decoder::NBit 5.514
- MIME::Decoder::QuotedPrint 5.514
- MIME::Decoder::UU 5.514
- MIME::Entity 5.514
- MIME::Field::ConTraEnc 5.514
- MIME::Field::ContDisp 5.514
- MIME::Field::ContType 5.514
- MIME::Field::ParamVal 5.514
- MIME::Head 5.514
- MIME::Parser 5.514
+ 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
+ MIME-tools-5.515
+ pathname: D/DS/DSKOLL/MIME-tools-5.515.tar.gz
+ provides:
+ MIME::Body 5.515
+ MIME::Body::File 5.515
+ MIME::Body::InCore 5.515
+ MIME::Body::Scalar 5.515
+ MIME::Decoder 5.515
+ MIME::Decoder::Base64 5.515
+ MIME::Decoder::BinHex 5.515
+ MIME::Decoder::Binary 5.515
+ MIME::Decoder::Gzip64 5.515
+ MIME::Decoder::NBit 5.515
+ MIME::Decoder::QuotedPrint 5.515
+ MIME::Decoder::UU 5.515
+ MIME::Entity 5.515
+ MIME::Field::ConTraEnc 5.515
+ MIME::Field::ContDisp 5.515
+ MIME::Field::ContType 5.515
+ MIME::Field::ParamVal 5.515
+ MIME::Head 5.515
+ MIME::Parser 5.515
MIME::Parser::FileInto undef
MIME::Parser::FileUnder undef
MIME::Parser::Filer undef
MIME::Parser::Reader undef
MIME::Parser::Results undef
- MIME::Tools 5.514
+ MIME::Tools 5.515
MIME::WordDecoder undef
MIME::WordDecoder::ISO_8859 undef
MIME::WordDecoder::US_ASCII undef
MIME::WordDecoder::UTF_8 undef
- MIME::Words 5.514
+ MIME::Words 5.515
requirements:
ExtUtils::MakeMaker 6.59
File::Path 1
@@ -1616,39 +1679,53 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
perl 5.006
- MailTools-2.21
- pathname: M/MA/MARKOV/MailTools-2.21.tar.gz
- provides:
- Mail::Address 2.21
- Mail::Cap 2.21
- Mail::Field 2.21
- Mail::Field::AddrList 2.21
- Mail::Field::Date 2.21
- Mail::Field::Generic 2.21
- Mail::Filter 2.21
- Mail::Header 2.21
- Mail::Internet 2.21
- Mail::Mailer 2.21
- Mail::Mailer::qmail 2.21
- Mail::Mailer::rfc822 2.21
- Mail::Mailer::sendmail 2.21
- Mail::Mailer::smtp 2.21
- Mail::Mailer::smtp::pipe 2.21
- Mail::Mailer::smtps 2.21
- Mail::Mailer::smtps::pipe 2.21
- Mail::Mailer::testfile 2.21
- Mail::Mailer::testfile::pipe 2.21
- Mail::Send 2.21
- Mail::Util 2.21
- MailTools 2.21
+ MailTools-2.22
+ pathname: M/MA/MARKOV/MailTools-2.22.tar.gz
+ provides:
+ Mail::Address 2.22
+ Mail::Cap 2.22
+ Mail::Field 2.22
+ Mail::Field::AddrList 2.22
+ Mail::Field::Date 2.22
+ Mail::Field::Generic 2.22
+ Mail::Filter 2.22
+ Mail::Header 2.22
+ Mail::Internet 2.22
+ Mail::Mailer 2.22
+ Mail::Mailer::qmail 2.22
+ Mail::Mailer::rfc822 2.22
+ Mail::Mailer::sendmail 2.22
+ Mail::Mailer::smtp 2.22
+ Mail::Mailer::smtp::pipe 2.22
+ Mail::Mailer::smtps 2.22
+ Mail::Mailer::smtps::pipe 2.22
+ Mail::Mailer::testfile 2.22
+ Mail::Mailer::testfile::pipe 2.22
+ Mail::Send 2.22
+ Mail::Util 2.22
+ MailTools 2.22
requirements:
Date::Format 0
Date::Parse 0
ExtUtils::MakeMaker 0
IO::Handle 0
Net::Domain 1.05
- Net::SMTP 1.03
+ Net::SMTP 1.28
Test::More 0
+ Math-Polygon-1.11
+ pathname: M/MA/MARKOV/Math-Polygon-1.11.tar.gz
+ provides:
+ Math::Polygon 1.11
+ Math::Polygon::Calc 1.11
+ Math::Polygon::Clip 1.11
+ Math::Polygon::Convex 1.11
+ Math::Polygon::Surface 1.11
+ Math::Polygon::Transform 1.11
+ requirements:
+ ExtUtils::MakeMaker 0
+ Math::Trig 0
+ Scalar::Util 1.13
+ Test::More 0.47
Module-Build-0.4234
pathname: L/LE/LEONT/Module-Build-0.4234.tar.gz
provides:
@@ -1694,10 +1771,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
@@ -1730,11 +1807,11 @@ DISTRIBUTIONS
Try::Tiny 0
strict 0
warnings 0
- Module-Pluggable-5.2
- pathname: S/SI/SIMONW/Module-Pluggable-5.2.tar.gz
+ Module-Pluggable-6.3
+ pathname: S/SI/SIMONW/Module-Pluggable-6.3.tar.gz
provides:
Devel::InnerPackage 0.4
- Module::Pluggable 5.2
+ Module::Pluggable 6.3
Module::Pluggable::Object 5.2
requirements:
Exporter 5.57
@@ -1743,19 +1820,17 @@ DISTRIBUTIONS
File::Find 0
File::Spec 3.00
File::Spec::Functions 0
+ Scalar::Util 0
if 0
- perl 5.005030
+ perl 5.006
strict 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
+ ExtUtils::MakeMaker 0
+ perl 5.006000
Mojo-Pg-4.27
pathname: S/SR/SRI/Mojo-Pg-4.27.tar.gz
provides:
@@ -1771,14 +1846,15 @@ DISTRIBUTIONS
Mojolicious 8.50
SQL::Abstract::Pg 1.0
perl 5.016
- Mojolicious-9.36
- pathname: S/SR/SRI/Mojolicious-9.36.tar.gz
+ 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
@@ -1840,7 +1916,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
@@ -1942,12 +2018,6 @@ DISTRIBUTIONS
requirements:
ExtUtils::MakeMaker 0
Module::Runtime 0.014
- Mozilla-CA-20240313
- pathname: L/LW/LWP/Mozilla-CA-20240313.tar.gz
- provides:
- Mozilla::CA 20240313
- requirements:
- ExtUtils::MakeMaker 0
Net-HTTP-6.23
pathname: O/OA/OALDERS/Net-HTTP-6.23.tar.gz
provides:
@@ -2038,6 +2108,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:
@@ -2056,11 +2145,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
@@ -2083,10 +2172,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
@@ -2134,52 +2223,57 @@ DISTRIBUTIONS
ExtUtils::MakeMaker 0
SQL::Abstract 2.0
perl 5.016
- 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.51
+ pathname: D/DR/DROLSKY/Specio-0.51.tar.gz
+ provides:
+ Specio 0.51
+ Specio::Coercion 0.51
+ Specio::Constraint::AnyCan 0.51
+ Specio::Constraint::AnyDoes 0.51
+ Specio::Constraint::AnyIsa 0.51
+ Specio::Constraint::Enum 0.51
+ Specio::Constraint::Intersection 0.51
+ Specio::Constraint::ObjectCan 0.51
+ Specio::Constraint::ObjectDoes 0.51
+ Specio::Constraint::ObjectIsa 0.51
+ Specio::Constraint::Parameterizable 0.51
+ Specio::Constraint::Parameterized 0.51
+ Specio::Constraint::Role::CanType 0.51
+ Specio::Constraint::Role::DoesType 0.51
+ Specio::Constraint::Role::Interface 0.51
+ Specio::Constraint::Role::IsaType 0.51
+ Specio::Constraint::Simple 0.51
+ Specio::Constraint::Structurable 0.51
+ Specio::Constraint::Structured 0.51
+ Specio::Constraint::Union 0.51
+ Specio::Declare 0.51
+ Specio::DeclaredAt 0.51
+ Specio::Exception 0.51
+ Specio::Exporter 0.51
+ Specio::Helpers 0.51
+ Specio::Library::Builtins 0.51
+ Specio::Library::Numeric 0.51
+ Specio::Library::Perl 0.51
+ Specio::Library::String 0.51
+ Specio::Library::Structured 0.51
+ Specio::Library::Structured::Dict 0.51
+ Specio::Library::Structured::Map 0.51
+ Specio::Library::Structured::Tuple 0.51
+ Specio::OO 0.51
+ Specio::PP 0.51
+ Specio::PartialDump 0.51
+ Specio::Registry 0.51
+ Specio::Role::Inlinable 0.51
+ Specio::Subs 0.51
+ Specio::TypeChecks 0.51
+ Specio::XS 0.51
+ Test::Specio 0.51
requirements:
B 0
Carp 0
+ Clone 0
+ Clone::Choose 0
+ Clone::PP 0
Devel::StackTrace 0
Eval::Closure 0
Exporter 0
@@ -2187,11 +2281,11 @@ DISTRIBUTIONS
IO::File 0
List::Util 1.33
MRO::Compat 0
+ Module::Implementation 0
Module::Runtime 0
Role::Tiny 1.003003
Role::Tiny::With 0
Scalar::Util 0
- Storable 0
Sub::Quote 0
Test::Fatal 0
Test::More 0.96
@@ -2224,13 +2318,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:
@@ -2263,71 +2350,70 @@ DISTRIBUTIONS
perl 5.006
strict 0
warnings 0
- 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-Deep-1.204
- pathname: R/RJ/RJBS/Test-Deep-1.204.tar.gz
- provides:
- Test::Deep 1.204
- Test::Deep::All 1.204
- Test::Deep::Any 1.204
- Test::Deep::Array 1.204
- Test::Deep::ArrayEach 1.204
- Test::Deep::ArrayElementsOnly 1.204
- Test::Deep::ArrayLength 1.204
- Test::Deep::ArrayLengthOnly 1.204
- Test::Deep::Blessed 1.204
- Test::Deep::Boolean 1.204
- Test::Deep::Cache 1.204
- Test::Deep::Cache::Simple 1.204
- Test::Deep::Class 1.204
- Test::Deep::Cmp 1.204
- Test::Deep::Code 1.204
- Test::Deep::Hash 1.204
- Test::Deep::HashEach 1.204
- Test::Deep::HashElements 1.204
- Test::Deep::HashKeys 1.204
- Test::Deep::HashKeysOnly 1.204
- Test::Deep::Ignore 1.204
- Test::Deep::Isa 1.204
- Test::Deep::ListMethods 1.204
- Test::Deep::MM 1.204
- Test::Deep::Methods 1.204
- Test::Deep::NoTest 1.204
- Test::Deep::None 1.204
- Test::Deep::Number 1.204
- Test::Deep::Obj 1.204
- Test::Deep::Ref 1.204
- Test::Deep::RefType 1.204
- Test::Deep::Regexp 1.204
- Test::Deep::RegexpMatches 1.204
- Test::Deep::RegexpOnly 1.204
- Test::Deep::RegexpRef 1.204
- Test::Deep::RegexpRefOnly 1.204
- Test::Deep::RegexpVersion 1.204
- Test::Deep::ScalarRef 1.204
- Test::Deep::ScalarRefOnly 1.204
- Test::Deep::Set 1.204
- Test::Deep::Shallow 1.204
- Test::Deep::Stack 1.204
- Test::Deep::String 1.204
- Test::Deep::SubHash 1.204
- Test::Deep::SubHashElements 1.204
- Test::Deep::SubHashKeys 1.204
- Test::Deep::SubHashKeysOnly 1.204
- Test::Deep::SuperHash 1.204
- Test::Deep::SuperHashElements 1.204
- Test::Deep::SuperHashKeys 1.204
- Test::Deep::SuperHashKeysOnly 1.204
+ Test-Deep-1.205
+ pathname: R/RJ/RJBS/Test-Deep-1.205.tar.gz
+ provides:
+ Test::Deep 1.205
+ Test::Deep::All 1.205
+ Test::Deep::Any 1.205
+ Test::Deep::Array 1.205
+ Test::Deep::ArrayEach 1.205
+ Test::Deep::ArrayElementsOnly 1.205
+ Test::Deep::ArrayLength 1.205
+ Test::Deep::ArrayLengthOnly 1.205
+ Test::Deep::Blessed 1.205
+ Test::Deep::Boolean 1.205
+ Test::Deep::Cache 1.205
+ Test::Deep::Cache::Simple 1.205
+ Test::Deep::Class 1.205
+ Test::Deep::Cmp 1.205
+ Test::Deep::Code 1.205
+ Test::Deep::Hash 1.205
+ Test::Deep::HashEach 1.205
+ Test::Deep::HashElements 1.205
+ Test::Deep::HashKeys 1.205
+ Test::Deep::HashKeysOnly 1.205
+ Test::Deep::Ignore 1.205
+ Test::Deep::Isa 1.205
+ Test::Deep::ListMethods 1.205
+ Test::Deep::MM 1.205
+ Test::Deep::Methods 1.205
+ Test::Deep::NoTest 1.205
+ Test::Deep::None 1.205
+ Test::Deep::Number 1.205
+ Test::Deep::Obj 1.205
+ Test::Deep::Ref 1.205
+ Test::Deep::RefType 1.205
+ Test::Deep::Regexp 1.205
+ Test::Deep::RegexpMatches 1.205
+ Test::Deep::RegexpOnly 1.205
+ Test::Deep::RegexpRef 1.205
+ Test::Deep::RegexpRefOnly 1.205
+ Test::Deep::RegexpVersion 1.205
+ Test::Deep::ScalarRef 1.205
+ Test::Deep::ScalarRefOnly 1.205
+ Test::Deep::Set 1.205
+ Test::Deep::Shallow 1.205
+ Test::Deep::Stack 1.205
+ Test::Deep::String 1.205
+ Test::Deep::SubHash 1.205
+ Test::Deep::SubHashElements 1.205
+ Test::Deep::SubHashKeys 1.205
+ Test::Deep::SubHashKeysOnly 1.205
+ Test::Deep::SuperHash 1.205
+ Test::Deep::SuperHashElements 1.205
+ Test::Deep::SuperHashKeys 1.205
+ Test::Deep::SuperHashKeysOnly 1.205
requirements:
ExtUtils::MakeMaker 6.78
List::Util 1.09
@@ -2397,12 +2483,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
@@ -2532,37 +2618,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.03
- pathname: D/DE/DERF/Travel-Status-DE-DeutscheBahn-6.03.tar.gz
- provides:
- Travel::Status::DE::DeutscheBahn 6.03
- Travel::Status::DE::HAFAS 6.03
- Travel::Status::DE::HAFAS::Journey 6.03
- Travel::Status::DE::HAFAS::Location 6.03
- Travel::Status::DE::HAFAS::Message 6.03
- Travel::Status::DE::HAFAS::Polyline 6.03
- Travel::Status::DE::HAFAS::Product 6.03
- Travel::Status::DE::HAFAS::Stop 6.03
- Travel::Status::DE::HAFAS::StopFinder 6.03
+ 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
@@ -2580,12 +2673,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
@@ -2612,10 +2705,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.13
+ pathname: D/DE/DERF/Travel-Status-DE-VRR-3.13.tar.gz
+ provides:
+ Travel::Status::DE::EFA 3.13
+ Travel::Status::DE::EFA::Departure 3.13
+ Travel::Status::DE::EFA::Info 3.13
+ Travel::Status::DE::EFA::Line 3.13
+ Travel::Status::DE::EFA::Services 3.13
+ Travel::Status::DE::EFA::Stop 3.13
+ Travel::Status::DE::EFA::Trip 3.13
+ Travel::Status::DE::VRR 3.13
+ 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
@@ -2634,56 +2778,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
@@ -2691,6 +2842,7 @@ DISTRIBUTIONS
Encode 0
Exporter 5.57
ExtUtils::MakeMaker 0
+ MIME::Base32 0
MIME::Base64 2
Net::Domain 0
Scalar::Util 0
@@ -2894,32 +3046,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
@@ -2969,15 +3121,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/examples/travelynx.conf b/examples/travelynx.conf
index f8eaac0..fdcd03e 100644
--- a/examples/travelynx.conf
+++ b/examples/travelynx.conf
@@ -35,6 +35,34 @@
password => die("Changeme!"),
},
+ # Settings specific to the DBRIS bahn.de backend.
+ # Their journey endpoint (which is required for checkins) is behind an IP
+ # reputation filter, denying requests from most non-residential IP ranges.
+ # If needed, you can specify either a single SOCKS proxy or a set of
+ # SOCKS proxies here, and thus work around that limitation. If multiple
+ # proxies are specified, travelynx will choose a random one for each
+ # request. Note that DBRIS bahn.de requests to non-journey endpoints
+ # (such as the departure board) are always sent directly and not passed
+ # through the proxy / proxies specified here.
+ # "proxies" takes precedence over "proxy".
+ dbris => {
+ 'bahn.de' => {
+ # proxy => 'socks://127.0.0.1:18080', # <- either this
+ # proxies => ['socks://127.0.0.1:18080', 'socks://127.0.0.1:18081'],
+ },
+ },
+
+ # Settings specific to HAFAS backends.
+ # For instance, the PKP backend is hidden behind a GeoIP filter, hence
+ # travelynx only supports it if travelynx.conf either indicates that it
+ # is reachable or specifies a proxy.
+ hafas => {
+ PKP => {
+ # geoip_ok => 1, # <- either this
+ # proxy => 'socks://...', # <- or this
+ },
+ },
+
# These settings control the amount and (re)spawn behaviour of travelynx
# worker processes as well as IP, port, and PID file. They are suitable for
# up to a few dozen concurrent users. If your site has more traffic, you
diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm
index 4d04e9e..c8c96b8 100755
--- a/lib/Travelynx.pm
+++ b/lib/Travelynx.pm
@@ -1,6 +1,7 @@
package Travelynx;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -19,10 +20,13 @@ use JSON;
use List::Util;
use List::UtilsBy qw(uniq_by);
use List::MoreUtils qw(first_index);
-use Travel::Status::DE::DBWagenreihung;
+use Travel::Status::DE::DBRIS::Formation;
use Travelynx::Helper::DBDB;
+use Travelynx::Helper::DBRIS;
+use Travelynx::Helper::EFA;
use Travelynx::Helper::HAFAS;
use Travelynx::Helper::IRIS;
+use Travelynx::Helper::MOTIS;
use Travelynx::Helper::Sendmail;
use Travelynx::Helper::Traewelling;
use Travelynx::Model::InTransit;
@@ -157,11 +161,12 @@ sub startup {
cache_iris_main => sub {
my ($self) = @_;
- return Cache::File->new(
+ state $cache = Cache::File->new(
cache_root => $self->app->config->{cache}->{schedule},
default_expires => '6 hours',
lock_level => Cache::File::LOCK_LOCAL(),
);
+ return $cache;
}
);
@@ -169,22 +174,12 @@ sub startup {
cache_iris_rt => sub {
my ($self) = @_;
- return Cache::File->new(
+ state $cache = Cache::File->new(
cache_root => $self->app->config->{cache}->{realtime},
default_expires => '70 seconds',
lock_level => Cache::File::LOCK_LOCAL(),
);
- }
- );
-
- $self->attr(
- coordinates_by_station => sub {
- my $legacy_names = $self->app->renamed_station;
- my $location = $self->stations->get_latlon_by_name;
- while ( my ( $old_name, $new_name ) = each %{$legacy_names} ) {
- $location->{$old_name} = $location->{$new_name};
- }
- return $location;
+ return $cache;
}
);
@@ -192,15 +187,17 @@ sub startup {
# via https://github.com/marudor/bahn.expert/blob/main/src/server/coachSequence/TrainNames.ts
$self->attr(
ice_name => sub {
- my $id_to_name = JSON->new->utf8->decode(
- scalar read_file('share/ice_names.json') );
+ state $id_to_name = {
+ Travel::Status::DE::DBRIS::Formation::Group::name_to_designation(
+ )
+ };
return $id_to_name;
}
);
$self->attr(
renamed_station => sub {
- my $legacy_to_new = JSON->new->utf8->decode(
+ state $legacy_to_new = JSON->new->utf8->decode(
scalar read_file('share/old_station_names.json') );
return $legacy_to_new;
}
@@ -226,10 +223,39 @@ sub startup {
);
$self->helper(
+ efa => sub {
+ my ($self) = @_;
+ state $efa = Travelynx::Helper::EFA->new(
+ log => $self->app->log,
+ main_cache => $self->app->cache_iris_main,
+ realtime_cache => $self->app->cache_iris_rt,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
+ );
+ }
+ );
+
+ $self->helper(
+ dbris => sub {
+ my ($self) = @_;
+ state $dbris = Travelynx::Helper::DBRIS->new(
+ log => $self->app->log,
+ service_config => $self->app->config->{dbris},
+ cache => $self->app->cache_iris_rt,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
+ );
+ }
+ );
+
+ $self->helper(
hafas => sub {
my ($self) = @_;
state $hafas = Travelynx::Helper::HAFAS->new(
log => $self->app->log,
+ service_config => $self->app->config->{hafas},
main_cache => $self->app->cache_iris_main,
realtime_cache => $self->app->cache_iris_rt,
root_url => $self->base_url_for('/')->to_abs,
@@ -253,6 +279,20 @@ sub startup {
);
$self->helper(
+ motis => sub {
+ my ($self) = @_;
+ state $motis = Travelynx::Helper::MOTIS->new(
+ log => $self->app->log,
+ cache => $self->app->cache_iris_rt,
+ user_agent => $self->ua,
+ root_url => $self->base_url_for('/')->to_abs,
+ version => $self->app->config->{version},
+ time_zone => 'Europe/Berlin',
+ );
+ }
+ );
+
+ $self->helper(
traewelling => sub {
my ($self) = @_;
state $trwl = Travelynx::Model::Traewelling->new( pg => $self->pg );
@@ -297,13 +337,12 @@ sub startup {
journeys => sub {
my ($self) = @_;
state $journeys = Travelynx::Model::Journeys->new(
- log => $self->app->log,
- pg => $self->pg,
- in_transit => $self->in_transit,
- stats_cache => $self->journey_stats_cache,
- renamed_station => $self->app->renamed_station,
- latlon_by_station => $self->app->coordinates_by_station,
- stations => $self->stations,
+ log => $self->app->log,
+ pg => $self->pg,
+ in_transit => $self->in_transit,
+ stats_cache => $self->journey_stats_cache,
+ renamed_station => $self->app->renamed_station,
+ stations => $self->stations,
);
}
);
@@ -362,11 +401,12 @@ sub startup {
dbdb => sub {
my ($self) = @_;
state $dbdb = Travelynx::Helper::DBDB->new(
- log => $self->app->log,
- cache => $self->app->cache_iris_main,
- root_url => $self->base_url_for('/')->to_abs,
- user_agent => $self->ua,
- version => $self->app->config->{version},
+ log => $self->app->log,
+ main_cache => $self->app->cache_iris_main,
+ realtime_cache => $self->app->cache_iris_rt,
+ root_url => $self->base_url_for('/')->to_abs,
+ user_agent => $self->ua,
+ version => $self->app->config->{version},
);
}
);
@@ -408,11 +448,45 @@ sub startup {
);
$self->helper(
+ 'efa_load_icon' => sub {
+ my ( $self, $occupancy ) = @_;
+
+ my @symbols
+ = (
+ qw(help_outline person_outline people priority_high not_interested)
+ );
+
+ 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;
+ }
+
+ return $symbols[$occupancy] // 'help_outline';
+ }
+ );
+
+ $self->helper(
'load_icon' => sub {
my ( $self, $load ) = @_;
my $first = $load->{FIRST} // 0;
my $second = $load->{SECOND} // 0;
+ # DBRIS
+ if ( $first == 99 ) {
+ $first = 4;
+ }
+ if ( $second == 99 ) {
+ $second = 4;
+ }
+
my @symbols
= (
qw(help_outline person_outline people priority_high not_interested)
@@ -450,6 +524,7 @@ sub startup {
my $station = $opt{station};
my $train_id = $opt{train_id};
+ my $ts = $opt{ts};
my $uid = $opt{uid} // $self->current_user->{id};
my $db = $opt{db} // $self->pg->db;
my $hafas;
@@ -459,9 +534,18 @@ sub startup {
return Mojo::Promise->reject('You are already checked in');
}
- if ( $train_id =~ m{[|]} ) {
+ if ( $opt{dbris} ) {
+ return $self->_checkin_dbris_p(%opt);
+ }
+ if ( $opt{efa} ) {
+ return $self->_checkin_efa_p(%opt);
+ }
+ if ( $opt{hafas} ) {
return $self->_checkin_hafas_p(%opt);
}
+ if ( $opt{motis} ) {
+ return $self->_checkin_motis_p(%opt);
+ }
my $promise = Mojo::Promise->new;
@@ -493,7 +577,9 @@ sub startup {
db => $db,
departure_eva => $eva,
train => $train,
- route => [ $self->iris->route_diff($train) ],
+ route => [ $self->iris->route_diff($train) ],
+ backend_id =>
+ $self->stations->get_backend_id( iris => 1 ),
);
};
if ($@) {
@@ -506,6 +592,17 @@ sub startup {
# mustn't be called during a transaction
if ( not $opt{in_transaction} ) {
$self->add_route_timestamps( $uid, $train, 1 );
+ $self->add_wagonorder(
+ uid => $uid,
+ train_id => $train->train_id,
+ is_departure => 1,
+ eva => $eva,
+ datetime => $train->sched_departure,
+ train_type => $train->type,
+ train_no => $train->train_no
+ );
+ $self->add_stationinfo( $uid, 1, $train->train_id,
+ $eva );
$self->run_hook( $uid, 'checkin' );
}
@@ -525,18 +622,444 @@ sub startup {
);
$self->helper(
- '_checkin_hafas_p' => sub {
+ '_checkin_motis_p' => sub {
my ( $self, %opt ) = @_;
my $station = $opt{station};
my $train_id = $opt{train_id};
+ my $ts = $opt{ts};
my $uid = $opt{uid} // $self->current_user->{id};
my $db = $opt{db} // $self->pg->db;
my $hafas;
my $promise = Mojo::Promise->new;
+ $self->motis->get_trip_p(
+ service => $opt{motis},
+ trip_id => $train_id,
+ )->then(
+ sub {
+ my ($trip) = @_;
+ my $found_stopover;
+
+ for my $stopover ( $trip->stopovers ) {
+ if ( $stopover->stop->id eq $station ) {
+ $found_stopover = $stopover;
+
+ # Lines may serve the same stop several times.
+ # Keep looking until the scheduled departure
+ # matches the one passed while checking in.
+ if ( $ts
+ and $stopover->scheduled_departure->epoch
+ == $ts )
+ {
+ last;
+ }
+ }
+ }
+
+ if ( not $found_stopover ) {
+ $promise->reject(
+"Did not find stopover at '$station' within trip '$train_id'"
+ );
+ return;
+ }
+
+ for my $stopover ( $trip->stopovers ) {
+ $self->stations->add_or_update(
+ stop => $stopover->stop,
+ db => $db,
+ motis => $opt{motis},
+ );
+ }
+
+ $self->stations->add_or_update(
+ stop => $found_stopover->stop,
+ db => $db,
+ motis => $opt{motis},
+ );
+
+ eval {
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ journey => $trip,
+ stopover => $found_stopover,
+ data => { trip_id => $train_id },
+ backend_id => $self->stations->get_backend_id(
+ motis => $opt{motis}
+ ),
+ );
+ };
+
+ if ($@) {
+ $self->app->log->error(
+ "Checkin($uid): INSERT failed: $@");
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
+ }
+
+ my $polyline;
+ if ( $trip->polyline ) {
+ my @station_list;
+ my @coordinate_list;
+ for my $coordinate ( $trip->polyline ) {
+ if ( $coordinate->{stop} ) {
+ if ( not defined $coordinate->{stop}->{eva} ) {
+ die();
+ }
+
+ push(
+ @coordinate_list,
+ [
+ $coordinate->{lon},
+ $coordinate->{lat},
+ $coordinate->{stop}->{eva}
+ ]
+ );
+
+ push( @station_list,
+ $coordinate->{stop}->name );
+ }
+ else {
+ push( @coordinate_list,
+ [ $coordinate->{lon}, $coordinate->{lat} ]
+ );
+ }
+ }
+
+ # equal length → polyline only consists of straight
+ # lines between stops. that's not helpful.
+ if ( @station_list == @coordinate_list ) {
+ $self->log->debug( 'Ignoring polyline for '
+ . $trip->route_name
+ . ' as it only consists of straight lines between stops.'
+ );
+ }
+ else {
+ $polyline = {
+ from_eva =>
+ ( $trip->stopovers )[0]->stop->{eva},
+ to_eva => ( $trip->stopovers )[-1]->stop->{eva},
+ coords => \@coordinate_list,
+ };
+ }
+ }
+
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ );
+ }
+
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkin' );
+ }
+
+ $promise->resolve($trip);
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+ }
+ );
+
+ $self->helper(
+ '_checkin_dbris_p' => sub {
+ my ( $self, %opt ) = @_;
+
+ my $station = $opt{station};
+ my $train_id = $opt{train_id};
+ my $train_suffix = $opt{train_suffix};
+ my $ts = $opt{ts};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+ my $hafas;
+
+ my $promise = Mojo::Promise->new;
+
+ $self->dbris->get_journey_p(
+ trip_id => $train_id,
+ with_polyline => 1
+ )->then(
+ sub {
+ my ($journey) = @_;
+ my $found;
+ for my $stop ( $journey->route ) {
+ if ( $stop->eva eq $station ) {
+ $found = $stop;
+
+ # Lines may serve the same stop several times.
+ # Keep looking until the scheduled departure
+ # matches the one passed while checking in.
+ if ( $ts and $stop->sched_dep->epoch == $ts ) {
+ last;
+ }
+ }
+ }
+ if ( not $found ) {
+ $promise->reject(
+"Did not find stop '$station' within journey '$train_id'"
+ );
+ return;
+ }
+ for my $stop ( $journey->route ) {
+ $self->stations->add_or_update(
+ stop => $stop,
+ db => $db,
+ dbris => 'bahn.de',
+ );
+ }
+ eval {
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ journey => $journey,
+ stop => $found,
+ data => { trip_id => $train_id },
+ backend_id => $self->stations->get_backend_id(
+ dbris => 'bahn.de'
+ ),
+ train_suffix => $train_suffix,
+ );
+ };
+ if ($@) {
+ $self->app->log->error(
+ "Checkin($uid): INSERT failed: $@");
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
+ }
+
+ my $polyline;
+ if ( $journey->polyline ) {
+ my @station_list;
+ my @coordinate_list;
+ for my $coord ( $journey->polyline ) {
+ if ( $coord->{stop} ) {
+ push(
+ @coordinate_list,
+ [
+ $coord->{lon}, $coord->{lat},
+ $coord->{stop}->eva
+ ]
+ );
+ push( @station_list, $coord->{stop}->name );
+ }
+ else {
+ push( @coordinate_list,
+ [ $coord->{lon}, $coord->{lat} ] );
+ }
+ }
+
+ # equal length → polyline only consists of straight
+ # lines between stops. that's not helpful.
+ if ( @station_list == @coordinate_list ) {
+ $self->log->debug( 'Ignoring polyline for '
+ . $journey->train
+ . ' as it only consists of straight lines between stops.'
+ );
+ }
+ else {
+ $polyline = {
+ from_eva => ( $journey->route )[0]->eva,
+ to_eva => ( $journey->route )[-1]->eva,
+ coords => \@coordinate_list,
+ };
+ }
+ }
+
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ );
+ }
+
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkin' );
+ $self->add_wagonorder(
+ uid => $uid,
+ train_id => $train_id,
+ is_departure => 1,
+ eva => $found->eva,
+ datetime => $found->sched_dep,
+ train_type => $journey->type,
+ train_no => $journey->train_no,
+ );
+ $self->add_stationinfo( $uid, 1, $train_id,
+ $found->eva );
+ }
+
+ $promise->resolve($journey);
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+ }
+ );
+
+ $self->helper(
+ '_checkin_efa_p' => sub {
+ my ( $self, %opt ) = @_;
+ my $station = $opt{station};
+ my $trip_id = $opt{train_id};
+ my $ts = $opt{ts};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+
+ my $promise = Mojo::Promise->new;
+ $self->efa->get_journey_p(
+ service => $opt{efa},
+ trip_id => $trip_id
+ )->then(
+ sub {
+ my ($journey) = @_;
+
+ my $found;
+ for my $stop ( $journey->route ) {
+ if ( $stop->id_num == $station ) {
+ $found = $stop;
+
+ # Lines may serve the same stop several times.
+ # Keep looking until the scheduled departure
+ # matches the one passed while checking in.
+ if ( $ts and $stop->sched_dep->epoch == $ts ) {
+ last;
+ }
+ }
+ }
+ if ( not $found ) {
+ $promise->reject(
+"Did not find stop '$station' within journey '$trip_id'"
+ );
+ return;
+ }
+
+ for my $stop ( $journey->route ) {
+ $self->stations->add_or_update(
+ stop => $stop,
+ db => $db,
+ efa => $opt{efa},
+ );
+ }
+
+ eval {
+ $self->in_transit->add(
+ uid => $uid,
+ db => $db,
+ journey => $journey,
+ stop => $found,
+ trip_id => $trip_id,
+ backend_id => $self->stations->get_backend_id(
+ efa => $opt{efa}
+ ),
+ );
+ };
+ if ($@) {
+ $self->app->log->error(
+ "Checkin($uid): INSERT failed: $@");
+ $promise->reject( 'INSERT failed: ' . $@ );
+ return;
+ }
+
+ my $polyline;
+ if ( $journey->polyline ) {
+ my @station_list;
+ my @coordinate_list;
+ for my $coord ( $journey->polyline ) {
+ if ( $coord->{stop} ) {
+ push(
+ @coordinate_list,
+ [
+ $coord->{lon}, $coord->{lat},
+ $coord->{stop}->id_num
+ ]
+ );
+ push( @station_list,
+ $coord->{stop}->full_name );
+ }
+ else {
+ push( @coordinate_list,
+ [ $coord->{lon}, $coord->{lat} ] );
+ }
+ }
+
+ # equal length → polyline only consists of straight
+ # lines between stops. that's not helpful.
+ if ( @station_list == @coordinate_list ) {
+ $self->log->debug( 'Ignoring polyline for '
+ . $journey->line
+ . ' as it only consists of straight lines between stops.'
+ );
+ }
+ else {
+ $polyline = {
+ from_eva => ( $journey->route )[0]->id_num,
+ to_eva => ( $journey->route )[-1]->id_num,
+ coords => \@coordinate_list,
+ };
+ }
+ }
+
+ if ($polyline) {
+ $self->in_transit->set_polyline(
+ uid => $uid,
+ db => $db,
+ polyline => $polyline,
+ );
+ }
+
+ # mustn't be called during a transaction
+ if ( not $opt{in_transaction} ) {
+ $self->run_hook( $uid, 'checkin' );
+ }
+
+ $promise->resolve($journey);
+
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+ return $promise;
+ }
+ );
+
+ $self->helper(
+ '_checkin_hafas_p' => sub {
+ my ( $self, %opt ) = @_;
+
+ my $station = $opt{station};
+ my $train_id = $opt{train_id};
+ my $ts = $opt{ts};
+ my $uid = $opt{uid} // $self->current_user->{id};
+ my $db = $opt{db} // $self->pg->db;
+
+ my $promise = Mojo::Promise->new;
+
$self->hafas->get_journey_p(
+ service => $opt{hafas},
trip_id => $train_id,
with_polyline => 1
)->then(
@@ -548,26 +1071,38 @@ sub startup {
or $stop->loc->eva == $station )
{
$found = $stop;
- last;
+
+ # Lines may serve the same stop several times.
+ # Keep looking until the scheduled departure
+ # matches the one passed while checking in.
+ if ( $ts and $stop->sched_dep->epoch == $ts ) {
+ last;
+ }
}
}
if ( not $found ) {
$promise->reject(
- "Did not find journey $train_id at $station");
+"Did not find stop '$station' within journey '$train_id'"
+ );
return;
}
for my $stop ( $journey->route ) {
$self->stations->add_or_update(
- stop => $stop,
- db => $db,
+ stop => $stop,
+ db => $db,
+ hafas => $opt{hafas},
);
}
eval {
$self->in_transit->add(
- uid => $uid,
- db => $db,
- journey => $journey,
- stop => $found,
+ uid => $uid,
+ db => $db,
+ journey => $journey,
+ stop => $found,
+ data => { trip_id => $journey->id },
+ backend_id => $self->stations->get_backend_id(
+ hafas => $opt{hafas}
+ ),
);
};
if ($@) {
@@ -576,11 +1111,6 @@ sub startup {
$promise->reject( 'INSERT failed: ' . $@ );
return;
}
- $self->in_transit->update_data(
- uid => $uid,
- db => $db,
- data => { trip_id => $journey->id }
- );
my $polyline;
if ( $journey->polyline ) {
@@ -631,6 +1161,19 @@ sub startup {
# mustn't be called during a transaction
if ( not $opt{in_transaction} ) {
$self->run_hook( $uid, 'checkin' );
+ if ( $opt{hafas} eq 'DB' and $journey->class <= 16 ) {
+ $self->add_wagonorder(
+ uid => $uid,
+ train_id => $journey->id,
+ is_departure => 1,
+ eva => $found->loc->eva,
+ datetime => $found->sched_dep,
+ train_type => $journey->type,
+ train_no => $journey->number
+ );
+ $self->add_stationinfo( $uid, 1, $journey->id,
+ $found->loc->eva );
+ }
}
$promise->resolve($journey);
@@ -749,6 +1292,7 @@ sub startup {
my $db = $opt{db} // $self->pg->db;
my $user = $self->get_user_status( $uid, $db );
my $train_id = $user->{train_id};
+ my $hafas = $opt{hafas};
my $promise = Mojo::Promise->new;
@@ -759,8 +1303,7 @@ sub startup {
}
if ( not $user->{checked_in} and not $user->{cancelled} ) {
- return $promise->resolve( 0,
- 'You are not checked into any train' );
+ return $promise->resolve( 0, 'You are not checked in' );
}
if ( $dep_eva and $dep_eva != $user->{dep_eva} ) {
@@ -770,8 +1313,13 @@ sub startup {
return $promise->resolve( 0, 'race condition' );
}
- if ( $train_id =~ m{[|]} ) {
- return $self->_checkout_hafas_p(%opt);
+ if ( $user->{is_dbris}
+ or $user->{is_efa}
+ or $user->{is_hafas}
+ or $user->{is_motis}
+ or $train_id eq 'manual' )
+ {
+ return $self->_checkout_journey_p(%opt);
}
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
@@ -893,7 +1441,6 @@ sub startup {
uid => $uid,
db => $db,
train => $train,
- route => [ $self->iris->route_diff($train) ]
);
$has_arrived
@@ -905,7 +1452,7 @@ sub startup {
if (@unknown_stations) {
$self->app->log->warn(
sprintf(
-'Route of %s %s (%s -> %s) contains unknown stations: %s',
+'IRIS: Route of %s %s (%s -> %s) contains unknown stations: %s',
$train->type,
$train->train_no,
$train->origin,
@@ -992,6 +1539,17 @@ sub startup {
if ( not $opt{in_transaction} ) {
$self->run_hook( $uid, 'update' );
$self->add_route_timestamps( $uid, $train, 0, 1 );
+ $self->add_wagonorder(
+ uid => $uid,
+ train_id => $train->train_id,
+ is_arrival => 1,
+ eva => $new_checkout_station_id,
+ datetime => $train->sched_departure,
+ train_type => $train->type,
+ train_no => $train->train_no
+ );
+ $self->add_stationinfo( $uid, 0, $train->train_id,
+ $dep_eva, $new_checkout_station_id );
}
$promise->resolve( 1, undef );
return;
@@ -1010,7 +1568,7 @@ sub startup {
);
$self->helper(
- '_checkout_hafas_p' => sub {
+ '_checkout_journey_p' => sub {
my ( $self, %opt ) = @_;
my $station = $opt{station};
@@ -1036,7 +1594,7 @@ sub startup {
my $has_arrived;
for my $stop ( @{ $journey->{route_after} } ) {
if ( $station eq $stop->[0] or $station eq $stop->[1] ) {
- $found = 1;
+ $found = $stop;
$self->in_transit->set_arrival_eva(
uid => $uid,
db => $db,
@@ -1065,7 +1623,7 @@ sub startup {
last;
}
}
- if ( not $found ) {
+ if ( not $found and not $force ) {
return $promise->resolve( 1, 'station not found in route' );
}
@@ -1104,6 +1662,22 @@ sub startup {
uid => $uid
);
}
+ elsif ( $found and $found->[2]{isCancelled} ) {
+ $journey = $self->in_transit->get(
+ uid => $uid,
+ db => $db
+ );
+ $journey->{cancelled} = 1;
+ $self->journeys->add_from_in_transit(
+ db => $db,
+ journey => $journey
+ );
+ $self->in_transit->set_cancelled_destination(
+ uid => $uid,
+ db => $db,
+ cancelled_destination => $found->[0],
+ );
+ }
if ($tx) {
$tx->commit;
@@ -1218,6 +1792,123 @@ sub startup {
}
);
+ $self->helper(
+ 'add_wagonorder' => sub {
+ my ( $self, %opt ) = @_;
+
+ my $uid = $opt{uid};
+ my $train_id = $opt{train_id};
+ my $train_type = $opt{train_type};
+ my $train_no = $opt{train_no};
+ my $eva = $opt{eva};
+ my $datetime = $opt{datetime};
+
+ $uid //= $self->current_user->{id};
+
+ my $db = $self->pg->db;
+
+ if ( $datetime and $train_no ) {
+ $self->dbdb->has_wagonorder_p(%opt)->then(
+ sub {
+ return $self->dbdb->get_wagonorder_p(%opt);
+ }
+ )->then(
+ sub {
+ my ($wagonorder) = @_;
+
+ my $data = {};
+ my $user_data = {};
+
+ my $wr;
+ eval {
+ $wr
+ = Travel::Status::DE::DBRIS::Formation->new(
+ json => $wagonorder );
+ };
+
+ if ( $opt{is_departure}
+ and $wr
+ and not exists $wagonorder->{error} )
+ {
+ my $dt
+ = $opt{datetime}->clone->set_time_zone('UTC');
+ $data->{wagonorder_dep} = $wagonorder;
+ $data->{wagonorder_param} = {
+ time => $dt->rfc3339 =~ s{(?=Z)}{.000}r,
+ number => $opt{train_no},
+ evaNumber => $opt{eva},
+ administrationId => 80,
+ date => $dt->strftime('%Y-%m-%d'),
+ category => $opt{train_type},
+ };
+ $user_data->{wagongroups} = [];
+ for my $group ( $wr->groups ) {
+ my @wagons;
+ for my $wagon ( $group->carriages ) {
+ push(
+ @wagons,
+ {
+ id => $wagon->uic_id,
+ number => $wagon->number,
+ type => $wagon->type,
+ }
+ );
+ }
+ push(
+ @{ $user_data->{wagongroups} },
+ {
+ name => $group->name,
+ desc => $group->desc_short,
+ description => $group->description,
+ designation => $group->designation,
+ to => $group->destination,
+ type => $group->train_type,
+ no => $group->train_no,
+ wagons => [@wagons],
+ }
+ );
+ if ( $group->{name}
+ and $group->{name} eq 'ICE0304' )
+ {
+ $data->{wagonorder_pride} = 1;
+ }
+ }
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data,
+ train_id => $train_id,
+ );
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ db => $db,
+ user_data => $user_data,
+ train_id => $train_id,
+ );
+ }
+ elsif ( $opt{is_arrival}
+ and not exists $wagonorder->{error} )
+ {
+ $data->{wagonorder_arr} = $wagonorder;
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => $data,
+ train_id => $train_id,
+ );
+ }
+ return;
+ }
+ )->catch(
+ sub {
+ # no wagonorder? no problem.
+ return;
+ }
+ )->wait;
+ }
+ }
+ );
+
# This helper is only ever called from an IRIS context.
# HAFAS already has all relevant information.
$self->helper(
@@ -1241,19 +1932,33 @@ sub startup {
return;
}
- my $route = $in_transit->{route};
+ my $route = $in_transit->{route};
+ my $train_id = $train->train_id;
+
+ my $tripid_promise;
- $self->hafas->get_tripid_p( train => $train )->then(
+ if ( $in_transit->{data}{trip_id} ) {
+ $tripid_promise
+ = Mojo::Promise->resolve( $in_transit->{data}{trip_id} );
+ }
+ else {
+ $tripid_promise = $self->hafas->get_tripid_p( train => $train );
+ }
+
+ $tripid_promise->then(
sub {
my ($trip_id) = @_;
- $self->in_transit->update_data(
- uid => $uid,
- db => $db,
- data => { trip_id => $trip_id }
- );
+ if ( not $in_transit->{extra_data}{trip_id} ) {
+ $self->in_transit->update_data(
+ uid => $uid,
+ db => $db,
+ data => { trip_id => $trip_id },
+ train_id => $train_id,
+ );
+ }
- return $self->hafas->get_route_timestamps_p(
+ return $self->hafas->get_route_p(
train => $train,
trip_id => $trip_id,
with_polyline => (
@@ -1264,42 +1969,63 @@ sub startup {
}
)->then(
sub {
- my ( $route_data, $journey, $polyline ) = @_;
+ my ( $new_route, $journey, $polyline ) = @_;
+ my $db_route;
- for my $station ( @{$route} ) {
- if ( $station->[0]
- =~ m{^Betriebsstelle nicht bekannt (\d+)$} )
- {
- my $eva = $1;
- if ( $route_data->{$eva} ) {
- $station->[0] = $route_data->{$eva}{name};
- $station->[1] = $route_data->{$eva}{eva};
- }
- }
- if ( my $sd = $route_data->{ $station->[0] } ) {
- $station->[1] = $sd->{eva};
- if ( $station->[2]{isAdditional} ) {
- $sd->{isAdditional} = 1;
- }
- if ( $station->[2]{isCancelled} ) {
- $sd->{isCancelled} = 1;
- }
+ for my $stop ( $journey->route ) {
+ $self->stations->add_or_update(
+ stop => $stop,
+ db => $db,
+ iris => 1,
+ );
+ }
- # keep rt_dep / rt_arr if they are no longer present
- my %old;
- for my $k (qw(rt_arr rt_dep arr_delay dep_delay)) {
- $old{$k} = $station->[2]{$k};
- }
- $station->[2] = $sd;
- if ( not $station->[2]{rt_arr} ) {
- $station->[2]{rt_arr} = $old{rt_arr};
- $station->[2]{arr_delay} = $old{arr_delay};
+ for my $i ( 0 .. $#{$new_route} ) {
+ my $old_name = $route->[$i][0];
+ my $old_eva = $route->[$i][1];
+ my $old_entry = $route->[$i][2];
+ my $new_name = $new_route->[$i]->{name};
+ my $new_eva = $new_route->[$i]->{eva};
+ my $new_entry = $new_route->[$i];
+
+ if ( defined $old_name and $old_name eq $new_name ) {
+ if ( $old_entry->{rt_arr}
+ and not $new_entry->{rt_arr} )
+ {
+ $new_entry->{rt_arr} = $old_entry->{rt_arr};
+ $new_entry->{arr_delay}
+ = $old_entry->{arr_delay};
}
- if ( not $station->[2]{rt_dep} ) {
- $station->[2]{rt_dep} = $old{rt_dep};
- $station->[2]{dep_delay} = $old{dep_delay};
+ if ( $old_entry->{rt_dep}
+ and not $new_entry->{rt_dep} )
+ {
+ $new_entry->{rt_dep} = $old_entry->{rt_dep};
+ $new_entry->{dep_delay}
+ = $old_entry->{dep_delay};
}
}
+
+ push(
+ @{$db_route},
+ [
+ $new_name,
+ $new_eva,
+ {
+ sched_arr => $new_entry->{sched_arr},
+ rt_arr => $new_entry->{rt_arr},
+ arr_delay => $new_entry->{arr_delay},
+ sched_dep => $new_entry->{sched_dep},
+ rt_dep => $new_entry->{rt_dep},
+ dep_delay => $new_entry->{dep_delay},
+ tz_offset => $new_entry->{tz_offset},
+ isAdditional => $new_entry->{isAdditional},
+ isCancelled => $new_entry->{isCancelled},
+ load => $new_entry->{load},
+ lat => $new_entry->{lat},
+ lon => $new_entry->{lon},
+ }
+ ]
+ );
}
my @messages;
@@ -1318,7 +2044,7 @@ sub startup {
$self->in_transit->set_route_data(
uid => $uid,
db => $db,
- route => $route,
+ route => $db_route,
delay_messages => [
map { [ $_->[0]->epoch, $_->[1] ] }
$train->delay_messages
@@ -1328,6 +2054,7 @@ sub startup {
$train->qos_messages
],
him_messages => \@messages,
+ train_id => $train_id,
);
if ($polyline) {
@@ -1336,6 +2063,7 @@ sub startup {
db => $db,
polyline => $polyline,
old_id => $in_transit->{polyline_id},
+ train_id => $train_id,
);
}
@@ -1348,107 +2076,28 @@ sub startup {
return;
}
)->wait;
+ }
+ );
- if ( $train->sched_departure ) {
- $self->dbdb->has_wagonorder_p( $train->sched_departure,
- $train->train_no )->then(
- sub {
- my ($api) = @_;
- return $self->dbdb->get_wagonorder_p( $api,
- $train->sched_departure, $train->train_no );
- }
- )->then(
- sub {
- my ($wagonorder) = @_;
-
- my $data = {};
- my $user_data = {};
+ $self->helper(
+ 'add_stationinfo' => sub {
+ my ( $self, $uid, $is_departure, $train_id, $dep_eva, $arr_eva )
+ = @_;
- if ( $is_departure and not exists $wagonorder->{error} )
- {
- $data->{wagonorder_dep} = $wagonorder;
- $user_data->{wagongroups} = [];
- for my $group (
- @{
- $wagonorder->{data}{istformation}
- {allFahrzeuggruppe} // []
- }
- )
- {
- my @wagons;
- for
- my $wagon ( @{ $group->{allFahrzeug} // [] } )
- {
- push(
- @wagons,
- {
- id => $wagon->{fahrzeugnummer},
- number =>
- $wagon->{wagenordnungsnummer},
- type => $wagon->{fahrzeugtyp},
- }
- );
- }
- push(
- @{ $user_data->{wagongroups} },
- {
- name =>
- $group->{fahrzeuggruppebezeichnung},
- from =>
- $group->{startbetriebsstellename},
- to => $group->{zielbetriebsstellename},
- no => $group->{verkehrlichezugnummer},
- wagons => [@wagons],
- }
- );
- if ( $group->{fahrzeuggruppebezeichnung}
- and $group->{fahrzeuggruppebezeichnung} eq
- 'ICE0304' )
- {
- $data->{wagonorder_pride} = 1;
- }
- }
- $self->in_transit->update_data(
- uid => $uid,
- db => $db,
- data => $data
- );
- $self->in_transit->update_user_data(
- uid => $uid,
- db => $db,
- user_data => $user_data
- );
- }
- elsif ( not $is_departure
- and not exists $wagonorder->{error} )
- {
- $data->{wagonorder_arr} = $wagonorder;
- $self->in_transit->update_data(
- uid => $uid,
- db => $db,
- data => $data
- );
- }
- return;
- }
- )->catch(
- sub {
- # no wagonorder? no problem.
- return;
- }
- )->wait;
- }
+ $uid //= $self->current_user->{id};
+ my $db = $self->pg->db;
if ($is_departure) {
- $self->dbdb->get_stationinfo_p( $in_transit->{dep_eva} )->then(
+ $self->dbdb->get_stationinfo_p($dep_eva)->then(
sub {
my ($station_info) = @_;
my $data = { stationinfo_dep => $station_info };
$self->in_transit->update_data(
- uid => $uid,
- db => $db,
- data => $data
+ uid => $uid,
+ db => $db,
+ data => $data,
+ train_id => $train_id,
);
return;
}
@@ -1460,16 +2109,17 @@ sub startup {
)->wait;
}
- if ( $in_transit->{arr_eva} and not $is_departure ) {
- $self->dbdb->get_stationinfo_p( $in_transit->{arr_eva} )->then(
+ if ( $arr_eva and not $is_departure ) {
+ $self->dbdb->get_stationinfo_p($arr_eva)->then(
sub {
my ($station_info) = @_;
my $data = { stationinfo_arr => $station_info };
$self->in_transit->update_data(
- uid => $uid,
- db => $db,
- data => $data
+ uid => $uid,
+ db => $db,
+ data => $data,
+ train_id => $train_id,
);
return;
}
@@ -1493,6 +2143,17 @@ sub startup {
$ret =~ s{[{]tt[}]}{$opt{tt}}g;
$ret =~ s{[{]tn[}]}{$opt{tn}}g;
$ret =~ s{[{]id[}]}{$opt{id}}g;
+ $ret =~ s{[{]dbris[}]}{$opt{dbris}}g;
+ $ret =~ s{[{]efa[}]}{$opt{efa}}g;
+ $ret =~ s{[{]hafas[}]}{$opt{hafas}}g;
+ $ret =~ s{[{]motis[}]}{$opt{motis}}g;
+
+ if ( $opt{id} and not $opt{is_iris} ) {
+ $ret =~ s{[{]id_or_tttn[}]}{$opt{id}}g;
+ }
+ else {
+ $ret =~ s{[{]id_or_tttn[}]}{$opt{tt}$opt{tn}}g;
+ }
return $ret;
}
);
@@ -1523,14 +2184,14 @@ sub startup {
my $wr;
eval {
$wr
- = Travel::Status::DE::DBWagenreihung->new(
- from_json => $wagonorder );
+ = Travel::Status::DE::DBRIS::Formation->new(
+ json => $wagonorder );
};
if ( $wr
- and $wr->sections
+ and $wr->sectors
and defined $wr->direction )
{
- my $section_0 = ( $wr->sections )[0];
+ my $section_0 = ( $wr->sectors )[0];
my $direction = $wr->direction;
if ( $section_0->name eq 'A'
and $direction == 0 )
@@ -1593,6 +2254,7 @@ sub startup {
uid => $uid,
db => $db,
with_data => 1,
+ with_polyline => 1,
with_timestamps => 1,
with_visibility => 1,
postprocess => 1,
@@ -1669,11 +2331,11 @@ sub startup {
my $wr;
eval {
$wr
- = Travel::Status::DE::DBWagenreihung->new(
- from_json => $in_transit->{data}{wagonorder_dep} );
+ = Travel::Status::DE::DBRIS::Formation->new(
+ json => $in_transit->{data}{wagonorder_dep} );
};
if ( $wr
- and $wr->wagons
+ and $wr->carriages
and defined $wr->direction )
{
$ret->{wagonorder} = $wr;
@@ -1691,7 +2353,8 @@ sub startup {
if ( $latest_cancellation and $latest_cancellation->{cancelled} ) {
if (
my $station = $self->stations->get_by_eva(
- $latest_cancellation->{dep_eva}
+ $latest_cancellation->{dep_eva},
+ backend_id => $latest_cancellation->{backend_id},
)
)
{
@@ -1700,7 +2363,8 @@ sub startup {
}
if (
my $station = $self->stations->get_by_eva(
- $latest_cancellation->{arr_eva}
+ $latest_cancellation->{arr_eva},
+ backend_id => $latest_cancellation->{backend_id},
)
)
{
@@ -1715,14 +2379,20 @@ sub startup {
if ($latest) {
my $ts = $latest->{checkout_ts};
my $action_time = epoch_to_dt($ts);
- if ( my $station
- = $self->stations->get_by_eva( $latest->{dep_eva} ) )
+ if (
+ my $station = $self->stations->get_by_eva(
+ $latest->{dep_eva}, backend_id => $latest->{backend_id}
+ )
+ )
{
$latest->{dep_ds100} = $station->{ds100};
$latest->{dep_name} = $station->{name};
}
- if ( my $station
- = $self->stations->get_by_eva( $latest->{arr_eva} ) )
+ if (
+ my $station = $self->stations->get_by_eva(
+ $latest->{arr_eva}, backend_id => $latest->{backend_id}
+ )
+ )
{
$latest->{arr_ds100} = $station->{ds100};
$latest->{arr_name} = $station->{name};
@@ -1731,6 +2401,12 @@ sub startup {
checked_in => 0,
cancelled => 0,
cancellation => $latest_cancellation,
+ backend_id => $latest->{backend_id},
+ backend_name => $latest->{backend_name},
+ is_dbris => $latest->{is_dbris},
+ is_iris => $latest->{is_iris},
+ is_hafas => $latest->{is_hafas},
+ is_motis => $latest->{is_motis},
journey_id => $latest->{journey_id},
timestamp => $action_time,
timestamp_delta => $now->epoch - $action_time->epoch,
@@ -1742,6 +2418,7 @@ sub startup {
real_departure => epoch_to_dt( $latest->{real_dep_ts} ),
dep_ds100 => $latest->{dep_ds100},
dep_eva => $latest->{dep_eva},
+ dep_external_id => $latest->{dep_external_id},
dep_name => $latest->{dep_name},
dep_lat => $latest->{dep_lat},
dep_lon => $latest->{dep_lon},
@@ -1750,6 +2427,7 @@ sub startup {
real_arrival => epoch_to_dt( $latest->{real_arr_ts} ),
arr_ds100 => $latest->{arr_ds100},
arr_eva => $latest->{arr_eva},
+ arr_external_id => $latest->{arr_external_id},
arr_name => $latest->{arr_name},
arr_lat => $latest->{arr_lat},
arr_lon => $latest->{arr_lon},
@@ -1788,13 +2466,22 @@ sub startup {
$status->{checked_in}
or $status->{cancelled}
) ? \1 : \0,
- comment => $status->{comment},
+ comment => $status->{comment},
+ backend => {
+ id => $status->{backend_id},
+ type => $status->{is_dbris} ? 'DBRIS'
+ : $status->{is_hafas} ? 'HAFAS'
+ : $status->{is_motis} ? 'MOTIS'
+ : 'IRIS-TTS',
+ name => $status->{backend_name},
+ },
fromStation => {
ds100 => $status->{dep_ds100},
name => $status->{dep_name},
uic => $status->{dep_eva},
longitude => $status->{dep_lon},
latitude => $status->{dep_lat},
+ platform => $status->{dep_platform},
scheduledTime => $status->{sched_departure}
? $status->{sched_departure}->epoch
: undef,
@@ -1808,6 +2495,7 @@ sub startup {
uic => $status->{arr_eva},
longitude => $status->{arr_lon},
latitude => $status->{arr_lat},
+ platform => $status->{arr_platform},
scheduledTime => $status->{sched_arrival}
? $status->{sched_arrival}->epoch
: undef,
@@ -1896,6 +2584,7 @@ sub startup {
$self->log->debug(
"... checked in : $traewelling->{dep_name} $traewelling->{dep_eva} -> $traewelling->{arr_name} $traewelling->{arr_eva}"
);
+ $self->users->mark_seen( uid => $uid );
my $user_status = $self->get_user_status($uid);
if ( $user_status->{checked_in} ) {
$self->log->debug(
@@ -1903,224 +2592,74 @@ sub startup {
return $promise->resolve;
}
- if ( $traewelling->{category}
- !~ m{^ (?: national .* | regional .* | suburban ) $ }x )
- {
-
- my $db = $self->pg->db;
- my $tx = $db->begin;
-
- $self->checkin_p(
- station => $traewelling->{dep_eva},
- train_id => $traewelling->{trip_id},
- uid => $uid,
- in_transaction => 1,
- db => $db
- )->then(
- sub {
- $self->log->debug("... handled origin");
- return $self->checkout_p(
- station => $traewelling->{arr_eva},
- train_id => $traewelling->{trip_id},
- uid => $uid,
- in_transaction => 1,
- db => $db
- );
- }
- )->then(
- sub {
- my ( undef, $err ) = @_;
- if ($err) {
- $self->log->debug("... error: $err");
- return Mojo::Promise->reject($err);
- }
- $self->log->debug("... handled destination");
- if ( $traewelling->{message} ) {
- $self->in_transit->update_user_data(
- uid => $uid,
- db => $db,
- user_data =>
- { comment => $traewelling->{message} }
- );
- }
- $self->traewelling->log(
- uid => $uid,
- db => $db,
- message =>
-"Eingecheckt in $traewelling->{line} nach $traewelling->{arr_name}",
- status_id => $traewelling->{status_id},
- );
- $self->traewelling->set_latest_pull_status_id(
- uid => $uid,
- status_id => $traewelling->{status_id},
- db => $db
- );
-
- $tx->commit;
- $promise->resolve;
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->log->debug("... error: $err");
- $self->traewelling->log(
- uid => $uid,
- message =>
-"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $err",
- status_id => $traewelling->{status_id},
- is_error => 1
- );
- $promise->resolve;
- return;
- }
- )->wait;
- return $promise;
- }
-
- $self->iris->get_departures_p(
- station => $traewelling->{dep_eva},
- lookbehind => 60,
- lookahead => 40
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+
+ $self->_checkin_dbris_p(
+ station => $traewelling->{dep_eva},
+ train_id => $traewelling->{trip_id},
+ uid => $uid,
+ in_transaction => 1,
+ db => $db
)->then(
sub {
- my ($dep) = @_;
- my ( $train_ref, $train_id );
-
- if ( $dep->{errstr} ) {
- $self->traewelling->log(
- uid => $uid,
- message =>
-"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $dep->{errstr}",
- status_id => $traewelling->{status_id},
- is_error => 1,
- );
- $promise->resolve;
- return;
- }
-
- for my $train ( @{ $dep->{results} } ) {
- if ( $train->line ne $traewelling->{line} ) {
- next;
- }
- if ( not $train->sched_departure
- or $train->sched_departure->epoch
- != $traewelling->{dep_dt}->epoch )
- {
- next;
- }
- if (
- not
- List::Util::first { $_ eq $traewelling->{arr_name} }
- $train->route_post
- )
- {
- next;
- }
- $train_id = $train->train_id;
- $train_ref = $train;
- last;
- }
-
- if ( not $train_id ) {
- $self->log->debug(
- "... train $traewelling->{line} not found");
- $self->traewelling->log(
- uid => $uid,
- message =>
-"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: Zug nicht gefunden",
- status_id => $traewelling->{status_id},
- is_error => 1
- );
- return $promise->resolve;
- }
-
- $self->log->debug("... found train: $train_id");
-
- my $db = $self->pg->db;
- my $tx = $db->begin;
-
- $self->checkin_p(
- station => $traewelling->{dep_eva},
- train_id => $train_id,
+ $self->log->debug("... handled origin");
+ return $self->_checkout_journey_p(
+ station => $traewelling->{arr_eva},
+ train_id => $traewelling->{trip_id},
uid => $uid,
in_transaction => 1,
db => $db
- )->then(
- sub {
- $self->log->debug("... handled origin");
- return $self->checkout_p(
- station => $traewelling->{arr_eva},
- train_id => 0,
- uid => $uid,
- in_transaction => 1,
- db => $db
- );
- }
- )->then(
- sub {
- my ( undef, $err ) = @_;
- if ($err) {
- $self->log->debug("... error: $err");
- return Mojo::Promise->reject($err);
- }
- $self->log->debug("... handled destination");
- if ( $traewelling->{message} ) {
- $self->in_transit->update_user_data(
- uid => $uid,
- db => $db,
- user_data =>
- { comment => $traewelling->{message} }
- );
- }
- $self->traewelling->log(
- uid => $uid,
- db => $db,
- message =>
+ );
+ }
+ )->then(
+ sub {
+ my ( undef, $err ) = @_;
+ if ($err) {
+ $self->log->debug("... error: $err");
+ return Mojo::Promise->reject($err);
+ }
+ $self->log->debug("... handled destination");
+ if ( $traewelling->{message} ) {
+ $self->in_transit->update_user_data(
+ uid => $uid,
+ db => $db,
+ user_data => { comment => $traewelling->{message} }
+ );
+ }
+ $self->traewelling->log(
+ uid => $uid,
+ db => $db,
+ message =>
"Eingecheckt in $traewelling->{line} nach $traewelling->{arr_name}",
- status_id => $traewelling->{status_id},
- );
- $self->traewelling->set_latest_pull_status_id(
- uid => $uid,
- status_id => $traewelling->{status_id},
- db => $db
- );
+ status_id => $traewelling->{status_id},
+ );
- $tx->commit;
- $promise->resolve;
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->log->debug("... error: $err");
- $self->traewelling->log(
- uid => $uid,
- message =>
-"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $err",
- status_id => $traewelling->{status_id},
- is_error => 1
- );
- $promise->resolve;
- return;
- }
- )->wait;
+ $self->traewelling->set_latest_pull_status_id(
+ uid => $uid,
+ status_id => $traewelling->{status_id},
+ db => $db
+ );
+
+ $tx->commit;
+ $promise->resolve;
+ return;
}
)->catch(
sub {
- my ( $err, $dep ) = @_;
+ my ($err) = @_;
+ $self->log->debug("... error: $err");
$self->traewelling->log(
uid => $uid,
message =>
-"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $dep->{errstr}",
+"Konnte $traewelling->{line} nach $traewelling->{arr_name} nicht übernehmen: $err",
status_id => $traewelling->{status_id},
- is_error => 1,
+ is_error => 1
);
$promise->resolve;
return;
}
)->wait;
-
return $promise;
}
);
@@ -2133,8 +2672,6 @@ sub startup {
my $route_type = $opt{route_type} // 'polybee';
my $include_manual = $opt{include_manual} ? 1 : 0;
- my $location = $self->app->coordinates_by_station;
-
my $with_polyline = $route_type eq 'beeline' ? 0 : 1;
if ( not @journeys ) {
@@ -2150,12 +2687,19 @@ sub startup {
my $first_departure = $journeys[-1]->{rt_departure};
my $last_departure = $journeys[0]->{rt_departure};
- my @stations = List::Util::uniq map { $_->{to_name} } @journeys;
- push( @stations,
- List::Util::uniq map { $_->{from_name} } @journeys );
- @stations = List::Util::uniq @stations;
- my @station_coordinates = map { [ $location->{$_}, $_ ] }
- grep { exists $location->{$_} } @stations;
+ my @stations = uniq_by { $_->{name} } map {
+ {
+ name => $_->{to_name} // $_->{arr_name},
+ latlon => $_->{to_latlon} // $_->{arr_latlon},
+ },
+ {
+ name => $_->{from_name} // $_->{dep_name},
+ latlon => $_->{from_latlon} // $_->{dep_latlon}
+ }
+ } @journeys;
+
+ my @station_coordinates
+ = map { [ $_->{latlon}, $_->{name} ] } @stations;
my @station_pairs;
my @polylines;
@@ -2175,19 +2719,44 @@ sub startup {
for my $journey (@polyline_journeys) {
my @polyline = @{ $journey->{polyline} };
- my $from_eva = $journey->{from_eva};
- my $to_eva = $journey->{to_eva};
+ my $from_eva = $journey->{from_eva} // $journey->{dep_eva};
+ my $to_eva = $journey->{to_eva} // $journey->{arr_eva};
my $from_index
= first_index { $_->[2] and $_->[2] == $from_eva } @polyline;
my $to_index
= first_index { $_->[2] and $_->[2] == $to_eva } @polyline;
+ # Work around inconsistencies caused by a multiple EVA IDs mapping to the same station name
+ if ( $from_index == -1 ) {
+ for my $entry ( @{ $journey->{route} // [] } ) {
+ if ( $entry->[0] eq $journey->{from_name} ) {
+ $from_eva = $entry->[1];
+ $from_index
+ = first_index { $_->[2] and $_->[2] == $from_eva }
+ @polyline;
+ last;
+ }
+ }
+ }
+
+ if ( $to_index == -1 ) {
+ for my $entry ( @{ $journey->{route} // [] } ) {
+ if ( $entry->[0] eq $journey->{to_name} ) {
+ $to_eva = $entry->[1];
+ $to_index
+ = first_index { $_->[2] and $_->[2] == $to_eva }
+ @polyline;
+ last;
+ }
+ }
+ }
+
if ( $from_index == -1
or $to_index == -1 )
{
# Fall back to route
- delete $journey->{polyline};
+ push( @beeline_journeys, $journey );
next;
}
@@ -2199,7 +2768,6 @@ sub startup {
if ( $seen{$key} ) {
next;
}
-
$seen{$key} = 1;
# direction does not matter at the moment
@@ -2209,6 +2777,9 @@ sub startup {
. ( $to_index - $from_index );
$seen{$key} = 1;
+ if ( $from_index > $to_index ) {
+ ( $to_index, $from_index ) = ( $from_index, $to_index );
+ }
@polyline = @polyline[ $from_index .. $to_index ];
my @polyline_coords;
for my $coord (@polyline) {
@@ -2219,23 +2790,38 @@ sub startup {
for my $journey (@beeline_journeys) {
- my @route = map { $_->[0] } @{ $journey->{route} };
+ my @route = @{ $journey->{route} };
- my $from_index
- = first_index { $_ eq $journey->{from_name} } @route;
- my $to_index = first_index { $_ eq $journey->{to_name} } @route;
+ my $from_index = first_index {
+ ( $_->[1]
+ and $_->[1]
+ == ( $journey->{from_eva} // $journey->{dep_eva} ) )
+ or $_->[0] eq
+ ( $journey->{from_name} // $journey->{dep_name} )
+ }
+ @route;
+ my $to_index = first_index {
+ ( $_->[1]
+ and $_->[1]
+ == ( $journey->{to_eva} // $journey->{arr_eva} ) )
+ or $_->[0] eq
+ ( $journey->{to_name} // $journey->{arr_name} )
+ }
+ @route;
if ( $from_index == -1 ) {
my $rename = $self->app->renamed_station;
$from_index = first_index {
- ( $rename->{$_} // $_ ) eq $journey->{from_name}
+ ( $rename->{ $_->[0] } // $_->[0] ) eq
+ ( $journey->{from_name} // $journey->{dep_name} )
}
@route;
}
if ( $to_index == -1 ) {
my $rename = $self->app->renamed_station;
$to_index = first_index {
- ( $rename->{$_} // $_ ) eq $journey->{to_name}
+ ( $rename->{ $_->[0] } // $_->[0] ) eq
+ ( $journey->{to_name} // $journey->{arr_name} )
}
@route;
}
@@ -2258,7 +2844,8 @@ sub startup {
# and entered manually (-> beeline also shown on map, typically
# significantly differs from detailed route) -- unless the user
# sets include_manual, of course.
- if ( $journey->{edited} & 0x0010
+ if ( $journey->{edited}
+ and $journey->{edited} & 0x0010
and @route <= 2
and not $include_manual )
{
@@ -2269,7 +2856,7 @@ sub startup {
@route = @route[ $from_index .. $to_index ];
- my $key = join( '|', @route );
+ my $key = join( '|', map { $_->[0] } @route );
if ( $seen{$key} ) {
next;
@@ -2278,7 +2865,7 @@ sub startup {
$seen{$key} = 1;
# direction does not matter at the moment
- $seen{ join( '|', reverse @route ) } = 1;
+ $seen{ join( '|', reverse map { $_->[0] } @route ) } = 1;
my $prev_station = shift @route;
for my $station (@route) {
@@ -2287,14 +2874,17 @@ sub startup {
}
}
- @station_pairs = uniq_by { $_->[0] . '|' . $_->[1] } @station_pairs;
- @station_pairs = grep {
- exists $location->{ $_->[0] }
- and exists $location->{ $_->[1] }
- } @station_pairs;
@station_pairs
- = map { [ $location->{ $_->[0] }, $location->{ $_->[1] } ] }
+ = uniq_by { $_->[0][0] . '|' . $_->[1][0] } @station_pairs;
+ @station_pairs
+ = grep { defined $_->[0][2]{lat} and defined $_->[1][2]{lat} }
@station_pairs;
+ @station_pairs = map {
+ [
+ [ $_->[0][2]{lat}, $_->[0][2]{lon} ],
+ [ $_->[1][2]{lat}, $_->[1][2]{lon} ]
+ ]
+ } @station_pairs;
my $ret = {
skipped_journeys => \@skipped_journeys,
@@ -2351,6 +2941,7 @@ sub startup {
$r->get('/changelog')->to('static#changelog');
$r->get('/impressum')->to('static#imprint');
$r->get('/imprint')->to('static#imprint');
+ $r->get('/tos')->to('static#tos');
$r->get('/legend')->to('static#legend');
$r->get('/offline.html')->to('static#offline');
$r->get('/api/v1/:user_action/:token')->to('api#get_v1');
@@ -2358,11 +2949,14 @@ sub startup {
$r->get('/recover')->to('account#request_password_reset');
$r->get('/recover/:id/:token')->to('account#recover_password');
$r->get('/reg/:id/:token')->to('account#verify');
- $r->get('/status/:name')->to('profile#user_status');
- $r->get('/status/:name/:ts')->to('profile#user_status');
+ $r->get( '/status/:name' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'profile#user_status', format => undef );
+ $r->get( '/status/:name/:ts' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'profile#user_status', format => undef );
$r->get('/ajax/status/#name')->to('profile#status_card');
$r->get('/ajax/status/:name/:ts')->to('profile#status_card');
- $r->get('/p/:name')->to('profile#profile');
+ $r->get( '/p/:name' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'profile#profile', format => undef );
$r->get( '/p/:name/j/:id' => 'public_journey' )
->to('profile#journey_details');
$r->get('/.well-known/webfinger')->to('account#webfinger');
@@ -2408,13 +3002,15 @@ sub startup {
$authed_r->get('/account/hooks')->to('account#webhook');
$authed_r->get('/account/traewelling')->to('traewelling#settings');
$authed_r->get('/account/insight')->to('account#insight');
- $authed_r->get('/account/services')->to('account#services');
$authed_r->get('/ajax/status_card.html')->to('traveling#status_card');
- $authed_r->get('/cancelled')->to('traveling#cancelled');
+ $authed_r->get( '/cancelled' => [ format => [ 'html', 'json' ] ] )
+ ->to( 'traveling#cancelled', format => undef );
+ $authed_r->get('/checkin/add')->to('traveling#add_intransit_form');
$authed_r->get('/fgr')->to('passengerrights#list_candidates');
$authed_r->get('/account/password')->to('account#password_form');
$authed_r->get('/account/mail')->to('account#change_mail');
$authed_r->get('/account/name')->to('account#change_name');
+ $authed_r->get('/account/select_backend')->to('account#backend_form');
$authed_r->get('/export.json')->to('account#json_export');
$authed_r->get('/history.json')->to('traveling#json_history');
$authed_r->get('/history.csv')->to('traveling#csv_history');
@@ -2436,7 +3032,8 @@ sub startup {
$authed_r->post('/account/hooks')->to('account#webhook');
$authed_r->post('/account/traewelling')->to('traewelling#settings');
$authed_r->post('/account/insight')->to('account#insight');
- $authed_r->post('/account/services')->to('account#services');
+ $authed_r->post('/account/select_backend')->to('account#change_backend');
+ $authed_r->post('/checkin/add')->to('traveling#add_intransit_form');
$authed_r->post('/journey/add')->to('traveling#add_journey_form');
$authed_r->post('/journey/comment')->to('traveling#comment_form');
$authed_r->post('/journey/visibility')->to('traveling#visibility_form');
diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm
index d13b2a7..95d67f5 100644
--- a/lib/Travelynx/Command/database.pm
+++ b/lib/Travelynx/Command/database.pm
@@ -1,14 +1,19 @@
package Travelynx::Command::database;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
use DateTime;
use File::Slurp qw(read_file);
+use List::Util qw();
use JSON;
+use Travel::Status::DE::EFA;
+use Travel::Status::DE::HAFAS;
use Travel::Status::DE::IRIS::Stations;
+use Travel::Status::MOTIS;
has description => 'Initialize or upgrade database layout';
@@ -1918,7 +1923,7 @@ my @migrations = (
# v49 -> v50
# travelynx 2.0 introduced proper HAFAS support, so there is no need for
- # the 'FYI, here is some hAFAS data' kludge anymore.
+ # the 'FYI, here is some HAFAS data' kludge anymore.
sub {
my ($db) = @_;
$db->query(
@@ -1946,6 +1951,1263 @@ my @migrations = (
}
);
},
+
+ # v51 -> v52
+ # Explicitly encode backend type; preparation for multiple HAFAS backends
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ create table backends (
+ id smallserial not null primary key,
+ iris bool not null,
+ hafas bool not null,
+ efa bool not null,
+ ris bool not null,
+ name varchar(32) not null,
+ unique (iris, hafas, efa, ris, name)
+ );
+ insert into backends (id, iris, hafas, efa, ris, name) values (0, true, false, false, false, '');
+ insert into backends (id, iris, hafas, efa, ris, name) values (1, false, true, false, false, 'DB');
+ alter sequence backends_id_seq restart with 2;
+ alter table in_transit add column backend_id smallint references backends (id);
+ alter table journeys add column backend_id smallint references backends (id);
+ update in_transit set backend_id = 0 where train_id not like '%|%';
+ update journeys set backend_id = 0 where train_id not like '%|%';
+ update in_transit set backend_id = 1 where train_id like '%|%';
+ update journeys set backend_id = 1 where train_id like '%|%';
+ update journeys set backend_id = 1 where train_id = 'manual';
+ alter table in_transit alter column backend_id set not null;
+ alter table journeys alter column backend_id set not null;
+
+ drop view in_transit_str;
+ drop view journeys_str;
+ create view in_transit_str as select
+ user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.ris as is_ris,
+ backend.name as backend_name,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ left join backends as backend on backend_id = backend.id
+ ;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.ris as is_ris,
+ backend.name as backend_name,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ left join backends as backend on backend_id = backend.id
+ ;
+ update schema_version set version = 52;
+ }
+ );
+ },
+
+ # v52 -> v53
+ # Extend train_id to be compatible with more recent HAFAS versions
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view in_transit_str;
+ drop view journeys_str;
+ drop view follows_in_transit;
+ alter table in_transit alter column train_id type varchar(384);
+ alter table journeys alter column train_id type varchar(384);
+ create view in_transit_str as select
+ user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.ris as is_ris,
+ backend.name as backend_name,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ left join backends as backend on backend_id = backend.id
+ ;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.ris as is_ris,
+ backend.name as backend_name,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ left join backends as backend on backend_id = backend.id
+ ;
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva
+ left join stations as arr_station on checkout_station_id = arr_station.eva
+ order by checkin_time desc
+ ;
+ update schema_version set version = 53;
+ }
+ );
+ },
+
+ # v53 -> v54
+ # Retrofit lat/lon data onto routes logged before v2.7.8; ensure
+ # consistent name and eva entries as well.
+ sub {
+ my ($db) = @_;
+
+ say
+'Adding lat/lon to routes of journeys logged before v2.7.8 and improving consistency of name/eva data in very old route entries.';
+ say 'This may take a while ...';
+
+ my %legacy_to_new;
+ if ( -r 'share/old_station_names.json' ) {
+ %legacy_to_new = %{ JSON->new->utf8->decode(
+ scalar read_file('share/old_station_names.json')
+ )
+ };
+ }
+
+ my %latlon_by_eva;
+ my %latlon_by_name;
+ my $res = $db->select( 'stations', [ 'name', 'eva', 'lat', 'lon' ] );
+ while ( my $row = $res->hash ) {
+ $latlon_by_eva{ $row->{eva} } = $row;
+ $latlon_by_name{ $row->{name} } = $row;
+ }
+
+ my $total
+ = $db->select( 'journeys', 'count(*) as count' )->hash->{count};
+ my $count = 0;
+ my $total_no_eva = 0;
+ my $total_no_latlon = 0;
+
+ my $json = JSON->new;
+
+ $res = $db->select( 'journeys_str', [ 'route', 'journey_id' ] );
+ while ( my $row = $res->expand->hash ) {
+ my $no_eva = 0;
+ my $no_latlon = 0;
+ my $changed = 0;
+ my @route = @{ $row->{route} };
+ for my $stop (@route) {
+ my $name = $stop->[0];
+ my $eva = $stop->[1];
+
+ if ( not $eva and $stop->[2]{eva} ) {
+ $eva = $stop->[1] = 0 + $stop->[2]{eva};
+ }
+
+ if ( $stop->[2]{eva} and $eva and $eva == $stop->[2]{eva} ) {
+ delete $stop->[2]{eva};
+ }
+
+ if ( $stop->[2]{name} and $name eq $stop->[2]{name} ) {
+ delete $stop->[2]{name};
+ }
+
+ if ( not $eva ) {
+ if ( $latlon_by_name{$name} ) {
+ $eva = $stop->[1] = $latlon_by_name{$name}{eva};
+ $changed = 1;
+ }
+ elsif ( $legacy_to_new{$name}
+ and $latlon_by_name{ $legacy_to_new{$name} } )
+ {
+ $eva = $stop->[1]
+ = $latlon_by_name{ $legacy_to_new{$name} }{eva};
+ $stop->[2]{lat}
+ = $latlon_by_name{ $legacy_to_new{$name} }{lat};
+ $stop->[2]{lon}
+ = $latlon_by_name{ $legacy_to_new{$name} }{lon};
+ $changed = 1;
+ }
+ else {
+ $no_eva = 1;
+ }
+ }
+
+ if ( $stop->[2]{lat} and $stop->[2]{lon} ) {
+ next;
+ }
+
+ if ( $eva and $latlon_by_eva{$eva} ) {
+ $stop->[2]{lat} = $latlon_by_eva{$eva}{lat};
+ $stop->[2]{lon} = $latlon_by_eva{$eva}{lon};
+ $changed = 1;
+ }
+ elsif ( $latlon_by_name{$name} ) {
+ $stop->[2]{lat} = $latlon_by_name{$name}{lat};
+ $stop->[2]{lon} = $latlon_by_name{$name}{lon};
+ $changed = 1;
+ }
+ elsif ( $legacy_to_new{$name}
+ and $latlon_by_name{ $legacy_to_new{$name} } )
+ {
+ $stop->[2]{lat}
+ = $latlon_by_name{ $legacy_to_new{$name} }{lat};
+ $stop->[2]{lon}
+ = $latlon_by_name{ $legacy_to_new{$name} }{lon};
+ $changed = 1;
+ }
+ else {
+ $no_latlon = 1;
+ }
+ }
+ if ($no_eva) {
+ $total_no_eva += 1;
+ }
+ if ($no_latlon) {
+ $total_no_latlon += 1;
+ }
+ if ($changed) {
+ $db->update(
+ 'journeys',
+ {
+ route => $json->encode( \@route ),
+ },
+ { id => $row->{journey_id} }
+ );
+ }
+ if ( $count++ % 10000 == 0 ) {
+ printf( " %2.0f%% complete\n", $count * 100 / $total );
+ }
+ }
+ say ' done';
+ if ($total_no_eva) {
+ printf( " (%d of %d routes still lack some EVA IDs)\n",
+ $total_no_eva, $total );
+ }
+ if ($total_no_latlon) {
+ printf( " (%d of %d routes still lack some lat/lon data)\n",
+ $total_no_latlon, $total );
+ }
+
+ $db->query(
+ qq{
+ update schema_version set version = 54;
+ }
+ );
+ },
+
+ # v54 -> v55
+ # do not share stations between backends
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table schema_version add column hafas varchar(12);
+ alter table users drop column external_services;
+ alter table users add column backend_id smallint references backends (id) default 1;
+ alter table stations drop constraint stations_pkey;
+ alter table stations add unique (eva, source);
+ create index eva_by_source on stations (eva, source);
+ create index eva on stations (eva);
+ alter table related_stations drop constraint related_stations_eva_meta_key;
+ drop index rel_eva;
+ alter table related_stations add column backend_id smallint;
+ update related_stations set backend_id = 1;
+ alter table related_stations alter column backend_id set not null;
+ alter table related_stations add constraint backend_fk foreign key (backend_id) references backends (id);
+ alter table related_stations add unique (eva, meta, backend_id);
+ create index related_stations_eva_backend_key on related_stations (eva, backend_id);
+ }
+ );
+
+ # up until now, IRIS and DB HAFAS shared stations, with IRIS taking
+ # preference. As of v2.7, this is no longer the case. However, old DB
+ # HAFAS journeys may still reference IRIS-specific stations. So, we
+ # make all IRIS stations available as DB HAFAS stations as well.
+ my $total
+ = $db->select( 'stations', 'count(*) as count', { source => 0 } )
+ ->hash->{count};
+ my $count = 0;
+
+ # Caveat: If this is a fresh installation, there are no IRIS stations
+ # in the database yet. So we have to populate it first.
+ if ( not $total ) {
+ say
+'Preparing to untangle IRIS / HAFAS stations, this may take a while ...';
+ $total = scalar Travel::Status::DE::IRIS::Stations::get_stations();
+ for my $s ( Travel::Status::DE::IRIS::Stations::get_stations() ) {
+ my ( $ds100, $name, $eva, $lon, $lat ) = @{$s};
+ if ( $ENV{__TRAVELYNX_TEST_MINI_IRIS}
+ and ( $eva < 8000000 or $eva > 8000100 ) )
+ {
+ next;
+ }
+ $db->insert(
+ 'stations',
+ {
+ eva => $eva,
+ ds100 => $ds100,
+ name => $name,
+ lat => $lat,
+ lon => $lon,
+ source => 0,
+ archived => 0
+ },
+ );
+ if ( $count++ % 1000 == 0 ) {
+ printf( " %2.0f%% complete\n", $count * 100 / $total );
+ }
+ }
+ $count = 0;
+ }
+
+ say 'Untangling IRIS / HAFAS stations, this may take a while ...';
+ my $res = $db->query(
+ qq{
+ select eva, ds100, name, lat, lon, archived
+ from stations
+ where source = 0;
+ }
+ );
+ while ( my $row = $res->hash ) {
+ $db->insert(
+ 'stations',
+ {
+ eva => $row->{eva},
+ ds100 => $row->{ds100},
+ name => $row->{name},
+ lat => $row->{lat},
+ lon => $row->{lon},
+ archived => $row->{archived},
+ source => 1,
+ }
+ );
+ if ( $count++ % 1000 == 0 ) {
+ printf( " %2.0f%% complete\n", $count * 100 / $total );
+ }
+ }
+
+ # Occasionally, IRIS checkins refer to stations that are not part of
+ # the Travel::Status::DE::IRIS database. Add those as HAFAS stops to
+ # satisfy the upcoming foreign key constraints.
+
+ my %iris_has_eva;
+ $res = $db->query(qq{select eva from stations where source = 0;});
+ while ( my $row = $res->hash ) {
+ $iris_has_eva{ $row->{eva} } = 1;
+ }
+
+ my %hafas_by_eva;
+ $res = $db->query(qq{select * from stations where source = 1;});
+ while ( my $row = $res->hash ) {
+ $hafas_by_eva{ $row->{eva} } = $row;
+ }
+
+ my @iris_ref_stations;
+ $res
+ = $db->query(
+qq{select distinct checkin_station_id from journeys where backend_id = 0;}
+ );
+ while ( my $row = $res->hash ) {
+ push( @iris_ref_stations, $row->{checkin_station_id} );
+ }
+ $res
+ = $db->query(
+qq{select distinct checkout_station_id from journeys where backend_id = 0;}
+ );
+ while ( my $row = $res->hash ) {
+ push( @iris_ref_stations, $row->{checkout_station_id} );
+ }
+ $res
+ = $db->query(
+qq{select distinct checkin_station_id from in_transit where backend_id = 0;}
+ );
+ while ( my $row = $res->hash ) {
+ push( @iris_ref_stations, $row->{checkin_station_id} );
+ }
+ $res
+ = $db->query(
+qq{select distinct checkout_station_id from in_transit where backend_id = 0;}
+ );
+ while ( my $row = $res->hash ) {
+ if ( $row->{checkout_station_id} ) {
+ push( @iris_ref_stations, $row->{checkout_station_id} );
+ }
+ }
+
+ @iris_ref_stations = List::Util::uniq @iris_ref_stations;
+
+ for my $station (@iris_ref_stations) {
+ if ( not $iris_has_eva{$station} ) {
+ $hafas_by_eva{$station}{source} = 0;
+ $hafas_by_eva{$station}{archived} = 1;
+ $db->insert( 'stations', $hafas_by_eva{$station} );
+ }
+ }
+
+ $db->query(
+ qq{
+ alter table in_transit add constraint in_transit_checkin_eva_fk
+ foreign key (checkin_station_id, backend_id)
+ references stations (eva, source);
+ alter table in_transit add constraint in_transit_checkout_eva_fk
+ foreign key (checkout_station_id, backend_id)
+ references stations (eva, source);
+ alter table journeys add constraint journeys_checkin_eva_fk
+ foreign key (checkin_station_id, backend_id)
+ references stations (eva, source);
+ alter table journeys add constraint journeys_checkout_eva_fk
+ foreign key (checkout_station_id, backend_id)
+ references stations (eva, source);
+ drop view in_transit_str;
+ drop view journeys_str;
+ drop view follows_in_transit;
+ create view in_transit_str as select
+ user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.ris as is_ris,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join backends as backend on in_transit.backend_id = backend.id
+ ;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.ris as is_ris,
+ backend.name as backend_name, journeys.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source
+ left join backends as backend on journeys.backend_id = backend.id
+ ;
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ in_transit.backend_id as backend_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ order by checkin_time desc
+ ;
+ create view users_with_backend as select
+ users.id as id, users.name as name, status, public_level,
+ email, password, registered_at, last_seen,
+ deletion_requested, deletion_notified, use_history,
+ accept_follows, notifications, profile, backend_id, iris,
+ hafas, efa, ris, backend.name as backend_name
+ from users
+ left join backends as backend on users.backend_id = backend.id
+ ;
+ update schema_version set version = 55;
+ update schema_version set hafas = '0';
+ }
+ );
+ say
+ 'This travelynx instance now has support for non-DB HAFAS backends.';
+ say
+'If the migration fails due to a deadlock, re-run it after stopping all background workers';
+ },
+
+ # v55 -> v56
+ # include backend data in dumpstops command
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ create view stations_str as
+ select stations.name as name,
+ eva, lat, lon,
+ backends.name as backend,
+ iris as is_iris,
+ hafas as is_hafas,
+ efa as is_efa,
+ ris as is_ris
+ from stations
+ left join backends
+ on source = backends.id;
+ update schema_version set version = 56;
+ }
+ );
+ },
+
+ # v56 -> v57
+ # Berlin Hbf used to be divided between "Berlin Hbf" (8011160) and "Berlin
+ # Hbf (tief)" (8098160). Since 2024, both are called "Berlin Hbf".
+ # As there are some places in the IRIS backend where station names are
+ # mapped to EVA IDs, this is not good. As of 2.8.21, travelynx deals with
+ # this IRIS edge case (and probably similar edge cases in Karlsruhe).
+ # Rebuild stats to ensure no bogus data is in there.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ truncate journey_stats;
+ update schema_version set version = 57;
+ }
+ );
+ },
+
+ # v57 -> v58
+ # Add backend data to follows_in_transit
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view follows_in_transit;
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.ris as is_ris,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join backends as backend on in_transit.backend_id = backend.id
+ order by checkin_time desc
+ ;
+ update schema_version set version = 58;
+ }
+ );
+ },
+
+ # v58 -> v59
+ # DB HAFAS is dead. Default to DB IRIS for now.
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table users alter column backend_id set default 0;
+ update schema_version set version = 59;
+ }
+ );
+ },
+
+ # v59 -> v60
+ # Add bahn.de / DBRIS backend
+ sub {
+ my ($db) = @_;
+ $db->insert(
+ 'backends',
+ {
+ iris => 0,
+ hafas => 0,
+ efa => 0,
+ ris => 1,
+ name => 'bahn.de',
+ },
+ );
+ $db->query(
+ qq{
+ update schema_version set version = 60;
+ }
+ );
+ },
+
+ # v60 -> v61
+ # Rename "ris" / "is_ris" to "dbris" / "is_dbris", as it is DB-specific
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view in_transit_str;
+ drop view journeys_str;
+ drop view users_with_backend;
+ drop view follows_in_transit;
+ alter table backends rename column ris to dbris;
+ create view in_transit_str as select
+ user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join backends as backend on in_transit.backend_id = backend.id
+ ;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.name as backend_name, journeys.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source
+ left join backends as backend on journeys.backend_id = backend.id
+ ;
+ create view users_with_backend as select
+ users.id as id, users.name as name, status, public_level,
+ email, password, registered_at, last_seen,
+ deletion_requested, deletion_notified, use_history,
+ accept_follows, notifications, profile, backend_id, iris,
+ hafas, efa, dbris, backend.name as backend_name
+ from users
+ left join backends as backend on users.backend_id = backend.id
+ ;
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join backends as backend on in_transit.backend_id = backend.id
+ order by checkin_time desc
+ ;
+ update schema_version set version = 61;
+ }
+ );
+ },
+
+ # v61 -> v62
+ # Add MOTIS backend type, add RNV and transitous MOTIS backends
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table backends add column motis bool default false;
+ alter table schema_version add column motis varchar(12);
+
+ create table stations_external_ids (
+ eva serial not null primary key,
+ backend_id smallint not null,
+ external_id text not null,
+
+ unique (backend_id, external_id),
+ foreign key (eva, backend_id) references stations (eva, source)
+ );
+
+ create view stations_with_external_ids as select
+ stations.*, stations_external_ids.external_id
+ from stations
+ left join stations_external_ids on
+ stations.eva = stations_external_ids.eva and
+ stations.source = stations_external_ids.backend_id
+ ;
+
+ drop view in_transit_str;
+ drop view journeys_str;
+ drop view users_with_backend;
+ drop view follows_in_transit;
+
+ create view in_transit_str as select
+ user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.motis as is_motis,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ dep_station_external_id.external_id as dep_external_id,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ arr_station_external_id.external_id as arr_external_id,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and in_transit.backend_id = dep_station_external_id.backend_id
+ left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and in_transit.backend_id = arr_station_external_id.backend_id
+ left join backends as backend on in_transit.backend_id = backend.id
+ ;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.motis as is_motis,
+ backend.name as backend_name, journeys.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ dep_station_external_id.external_id as dep_external_id,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ arr_station_external_id.external_id as arr_external_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source
+ left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and journeys.backend_id = dep_station_external_id.backend_id
+ left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and journeys.backend_id = arr_station_external_id.backend_id
+ left join backends as backend on journeys.backend_id = backend.id
+ ;
+ create view users_with_backend as select
+ users.id as id, users.name as name, status, public_level,
+ email, password, registered_at, last_seen,
+ deletion_requested, deletion_notified, use_history,
+ accept_follows, notifications, profile, backend_id, iris,
+ hafas, efa, dbris, motis, backend.name as backend_name
+ from users
+ left join backends as backend on users.backend_id = backend.id
+ ;
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.motis as is_motis,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join backends as backend on in_transit.backend_id = backend.id
+ order by checkin_time desc
+ ;
+ }
+ );
+ $db->query(
+ qq{
+ update schema_version set version = 62;
+ }
+ );
+ },
+
+ # v62 -> v63
+ # Add EFA backend support
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ alter table schema_version add column efa varchar(12);
+ update schema_version set version = 63;
+ update schema_version set efa = '0';
+ }
+ );
+ },
+
+ # v63 -> v64
+ # Relax train_type length constraints for EFA and MOTIS checkins
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view in_transit_str;
+ drop view journeys_str;
+ drop view users_with_backend;
+ drop view follows_in_transit;
+
+ alter table in_transit alter column train_type type varchar(32);
+ alter table journeys alter column train_type type varchar(32);
+
+ create view in_transit_str as select
+ user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.motis as is_motis,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ dep_station_external_id.external_id as dep_external_id,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ arr_station_external_id.external_id as arr_external_id,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and in_transit.backend_id = dep_station_external_id.backend_id
+ left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and in_transit.backend_id = arr_station_external_id.backend_id
+ left join backends as backend on in_transit.backend_id = backend.id
+ ;
+ create view journeys_str as select
+ journeys.id as journey_id, user_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.motis as is_motis,
+ backend.name as backend_name, journeys.backend_id as backend_id,
+ train_type, train_line, train_no, train_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ dep_station_external_id.external_id as dep_external_id,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ arr_station_external_id.external_id as arr_external_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, edited, route, messages, user_data,
+ dep_platform, arr_platform
+ from journeys
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and journeys.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and journeys.backend_id = arr_station.source
+ left join stations_external_ids as dep_station_external_id on checkin_station_id = dep_station_external_id.eva and journeys.backend_id = dep_station_external_id.backend_id
+ left join stations_external_ids as arr_station_external_id on checkout_station_id = arr_station_external_id.eva and journeys.backend_id = arr_station_external_id.backend_id
+ left join backends as backend on journeys.backend_id = backend.id
+ ;
+ create view users_with_backend as select
+ users.id as id, users.name as name, status, public_level,
+ email, password, registered_at, last_seen,
+ deletion_requested, deletion_notified, use_history,
+ accept_follows, notifications, profile, backend_id, iris,
+ hafas, efa, dbris, motis, backend.name as backend_name
+ from users
+ left join backends as backend on users.backend_id = backend.id
+ ;
+ create view follows_in_transit as select
+ r1.subject_id as follower_id, user_id as followee_id,
+ users.name as followee_name,
+ train_type, train_line, train_no, train_id,
+ backend.iris as is_iris, backend.hafas as is_hafas,
+ backend.efa as is_efa, backend.dbris as is_dbris,
+ backend.motis as is_motis,
+ backend.name as backend_name, in_transit.backend_id as backend_id,
+ extract(epoch from checkin_time) as checkin_ts,
+ extract(epoch from sched_departure) as sched_dep_ts,
+ extract(epoch from real_departure) as real_dep_ts,
+ checkin_station_id as dep_eva,
+ dep_station.ds100 as dep_ds100,
+ dep_station.name as dep_name,
+ dep_station.lat as dep_lat,
+ dep_station.lon as dep_lon,
+ extract(epoch from checkout_time) as checkout_ts,
+ extract(epoch from sched_arrival) as sched_arr_ts,
+ extract(epoch from real_arrival) as real_arr_ts,
+ checkout_station_id as arr_eva,
+ arr_station.ds100 as arr_ds100,
+ arr_station.name as arr_name,
+ arr_station.lat as arr_lat,
+ arr_station.lon as arr_lon,
+ polyline_id,
+ polylines.polyline as polyline,
+ visibility,
+ coalesce(visibility, users.public_level & 127) as effective_visibility,
+ cancelled, route, messages, user_data,
+ dep_platform, arr_platform, data
+ from in_transit
+ left join polylines on polylines.id = polyline_id
+ left join users on users.id = user_id
+ left join relations as r1 on r1.predicate = 1 and r1.object_id = user_id
+ left join stations as dep_station on checkin_station_id = dep_station.eva and in_transit.backend_id = dep_station.source
+ left join stations as arr_station on checkout_station_id = arr_station.eva and in_transit.backend_id = arr_station.source
+ left join backends as backend on in_transit.backend_id = backend.id
+ order by checkin_time desc
+ ;
+
+ update schema_version set version = 64;
+ }
+ );
+ },
+
+ # v64 -> v65
+ # stations_str: add is_motis
+ sub {
+ my ($db) = @_;
+ $db->query(
+ qq{
+ drop view stations_str;
+ create view stations_str as
+ select stations.name as name,
+ eva, lat, lon,
+ backends.name as backend,
+ dbris as is_dbris,
+ efa as is_efa,
+ iris as is_iris,
+ hafas as is_hafas,
+ motis as is_motis
+ from stations
+ left join backends
+ on source = backends.id;
+ update schema_version set version = 65;
+ }
+ );
+ },
);
sub sync_stations {
@@ -1977,7 +3239,7 @@ sub sync_stations {
},
{
on_conflict => \
-'(eva) do update set archived = false, source = 0, ds100 = EXCLUDED.ds100, name=EXCLUDED.name, lat=EXCLUDED.lat, lon=EXCLUDED.lon'
+'(eva, source) do update set archived = false, source = 0, ds100 = EXCLUDED.ds100, name=EXCLUDED.name, lat=EXCLUDED.lat, lon=EXCLUDED.lon'
}
);
if ( $count++ % 1000 == 0 ) {
@@ -2136,6 +3398,99 @@ sub sync_stations {
}
}
+sub sync_backends_efa {
+ my ($db) = @_;
+ for my $service ( Travel::Status::DE::EFA::get_services() ) {
+ my $present = $db->select(
+ 'backends',
+ 'count(*) as count',
+ {
+ efa => 1,
+ name => $service->{shortname}
+ }
+ )->hash->{count};
+ if ( not $present ) {
+ $db->insert(
+ 'backends',
+ {
+ dbris => 0,
+ efa => 1,
+ hafas => 0,
+ iris => 0,
+ motis => 0,
+ name => $service->{shortname},
+ },
+ { on_conflict => undef }
+ );
+ }
+ }
+
+ $db->update( 'schema_version',
+ { efa => $Travel::Status::DE::EFA::VERSION } );
+}
+
+sub sync_backends_hafas {
+ my ($db) = @_;
+ for my $service ( Travel::Status::DE::HAFAS::get_services() ) {
+ my $present = $db->select(
+ 'backends',
+ 'count(*) as count',
+ {
+ hafas => 1,
+ name => $service->{shortname}
+ }
+ )->hash->{count};
+ if ( not $present ) {
+ $db->insert(
+ 'backends',
+ {
+ dbris => 0,
+ efa => 0,
+ hafas => 1,
+ iris => 0,
+ motis => 0,
+ name => $service->{shortname},
+ },
+ { on_conflict => undef }
+ );
+ }
+ }
+
+ $db->update( 'schema_version',
+ { hafas => $Travel::Status::DE::HAFAS::VERSION } );
+}
+
+sub sync_backends_motis {
+ my ($db) = @_;
+ for my $service ( Travel::Status::MOTIS::get_services() ) {
+ my $present = $db->select(
+ 'backends',
+ 'count(*) as count',
+ {
+ motis => 1,
+ name => $service->{shortname}
+ }
+ )->hash->{count};
+ if ( not $present ) {
+ $db->insert(
+ 'backends',
+ {
+ dbris => 0,
+ efa => 0,
+ hafas => 0,
+ iris => 0,
+ motis => 1,
+ name => $service->{shortname},
+ },
+ { on_conflict => undef }
+ );
+ }
+ }
+
+ $db->update( 'schema_version',
+ { motis => $Travel::Status::MOTIS::VERSION } );
+}
+
sub setup_db {
my ($db) = @_;
my $tx = $db->begin;
@@ -2202,9 +3557,9 @@ sub migrate_db {
}
my $iris_version = get_schema_version( $db, 'iris' );
- say "Found IRIS station database v${iris_version}";
+ say "Found IRIS station table v${iris_version}";
if ( $iris_version eq $Travel::Status::DE::IRIS::Stations::VERSION ) {
- say 'Station database is up-to-date';
+ say 'Station table is up-to-date';
}
else {
eval {
@@ -2223,6 +3578,39 @@ sub migrate_db {
}
}
+ my $efa_version = get_schema_version( $db, 'efa' );
+ say "Found backend table for EFA v${efa_version}";
+ if ( $efa_version eq $Travel::Status::DE::EFA::VERSION ) {
+ say 'Backend table is up-to-date';
+ }
+ else {
+ say
+"Synchronizing with Travel::Status::DE::EFA $Travel::Status::DE::EFA::VERSION";
+ sync_backends_efa($db);
+ }
+
+ my $hafas_version = get_schema_version( $db, 'hafas' );
+ say "Found backend table for HAFAS v${hafas_version}";
+ if ( $hafas_version eq $Travel::Status::DE::HAFAS::VERSION ) {
+ say 'Backend table is up-to-date';
+ }
+ else {
+ say
+"Synchronizing with Travel::Status::DE::HAFAS $Travel::Status::DE::HAFAS::VERSION";
+ sync_backends_hafas($db);
+ }
+
+ my $motis_version = get_schema_version( $db, 'motis' ) // '0';
+ say "Found backend table for Motis v${motis_version}";
+ if ( $motis_version eq $Travel::Status::MOTIS::VERSION ) {
+ say 'Backend table is up-to-date';
+ }
+ else {
+ say
+"Synchronizing with Travel::Status::MOTIS $Travel::Status::MOTIS::VERSION";
+ sync_backends_motis($db);
+ }
+
$db->update( 'schema_version',
{ travelynx => $self->app->config->{version} } );
diff --git a/lib/Travelynx/Command/dumpconfig.pm b/lib/Travelynx/Command/dumpconfig.pm
index 600ffb0..2c308c9 100644
--- a/lib/Travelynx/Command/dumpconfig.pm
+++ b/lib/Travelynx/Command/dumpconfig.pm
@@ -1,4 +1,5 @@
package Travelynx::Command::dumpconfig;
+
# Copyright (C) 2020-2023 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
diff --git a/lib/Travelynx/Command/dumpstops.pm b/lib/Travelynx/Command/dumpstops.pm
index e6740ec..15f5861 100644
--- a/lib/Travelynx/Command/dumpstops.pm
+++ b/lib/Travelynx/Command/dumpstops.pm
@@ -1,6 +1,6 @@
package Travelynx::Command::dumpstops;
-# Copyright (C) 2024 Birte Kristina Friesel
+# Copyright (C) 2024-2025 Birte Kristina Friesel
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -8,7 +8,7 @@ use Mojo::Base 'Mojolicious::Command';
use List::Util qw();
use Text::CSV;
-has description => 'Export HAFAS/IRIS stops to CSV';
+has description => 'Export known stops to CSV';
has usage => sub { shift->extract_usage };
@@ -24,12 +24,13 @@ sub run {
or die("open($filename): $!\n");
my $csv = Text::CSV->new( { eol => "\r\n" } );
- $csv->combine(qw(name eva lat lon source archived));
+ $csv->combine(qw(name eva lat lon backend is_dbris is_efa is_iris is_hafas is_motis));
print $fh $csv->string;
my $iter = $self->app->stations->get_db_iterator;
while ( my $row = $iter->hash ) {
- $csv->combine( @{$row}{qw{name eva lat lon source archived}} );
+ $csv->combine(
+ @{$row}{qw{name eva lat lon backend is_dbris is_efa is_iris is_hafas is_motis}} );
print $fh $csv->string;
}
close($fh);
diff --git a/lib/Travelynx/Command/influxdb.pm b/lib/Travelynx/Command/influxdb.pm
index f3fc3de..4b779a2 100644
--- a/lib/Travelynx/Command/influxdb.pm
+++ b/lib/Travelynx/Command/influxdb.pm
@@ -29,7 +29,7 @@ sub run {
my $active = $now->clone->subtract( months => 1 );
my @stats;
- my @stations;
+ my @backend_stats;
my @traewelling;
push(
@@ -85,50 +85,31 @@ sub run {
)
);
- push(
- @stations,
- query_to_influx(
- 'iris',
- $db->select(
- 'stations',
- 'count(*) as count',
- {
- source => 0,
- archived => 0
- }
- )->hash->{count}
- )
- );
- push(
- @stations,
- query_to_influx(
- 'hafas',
- $db->select(
- 'stations',
- 'count(*) as count',
- {
- source => 1,
- archived => 0
- }
- )->hash->{count}
- )
- );
- push(
- @stations,
- query_to_influx(
- 'archived',
- $db->select( 'stations', 'count(*) as count', { archived => 1 } )
- ->hash->{count}
- )
- );
- push(
- @stations,
- query_to_influx(
- 'meta',
- $db->select( 'related_stations', 'count(*) as count' )
- ->hash->{count}
- )
- );
+ my @backends = $self->app->stations->get_backends;
+
+ for my $backend (@backends) {
+ push(
+ @backend_stats,
+ [
+ $backend->{iris} ? 'IRIS' : $backend->{name},
+ $db->select(
+ 'stations',
+ 'count(*) as count',
+ {
+ source => $backend->{id},
+ archived => 0
+ }
+ )->hash->{count},
+ $db->select(
+ 'related_stations',
+ 'count(*) as count',
+ {
+ backend_id => $backend->{id},
+ }
+ )->hash->{count}
+ ]
+ );
+ }
push(
@traewelling,
@@ -167,10 +148,18 @@ sub run {
. $self->app->config->{influxdb}->{url}
. ' stats '
. join( ',', @stats ) );
- $self->app->log->debug( 'POST '
- . $self->app->config->{influxdb}->{url}
- . ' stations '
- . join( ',', @stations ) );
+ for my $backend_entry (@backend_stats) {
+ $self->app->log->debug(
+ 'POST '
+ . $self->app->config->{influxdb}->{url}
+ . ' stations,backend='
+ . $backend_entry->[0]
+ . sprintf(
+ ' count=%d,meta=%d',
+ $backend_entry->[1], $backend_entry->[2]
+ )
+ );
+ }
$self->app->log->debug( 'POST '
. $self->app->config->{influxdb}->{url}
. ' traewelling '
@@ -181,10 +170,16 @@ sub run {
$self->app->config->{influxdb}->{url},
'stats ' . join( ',', @stats )
)->wait;
- $self->app->ua->post_p(
- $self->app->config->{influxdb}->{url},
- 'stations ' . join( ',', @stations )
- )->wait;
+ my $buf = q{};
+ for my $backend_entry (@backend_stats) {
+ $buf
+ .= "\nstations,backend="
+ . $backend_entry->[0]
+ . sprintf( ' count=%d,meta=%d',
+ $backend_entry->[1], $backend_entry->[2] );
+ }
+ $self->app->ua->post_p( $self->app->config->{influxdb}->{url}, $buf )
+ ->wait;
$self->app->ua->post_p(
$self->app->config->{influxdb}->{url},
'traewelling ' . join( ',', @traewelling )
diff --git a/lib/Travelynx/Command/integritycheck.pm b/lib/Travelynx/Command/integritycheck.pm
index 4894c3d..be5fe71 100644
--- a/lib/Travelynx/Command/integritycheck.pm
+++ b/lib/Travelynx/Command/integritycheck.pm
@@ -9,58 +9,60 @@ use List::Util qw();
use Travel::Status::DE::IRIS::Stations;
sub run {
- my ($self) = @_;
- my $found = 0;
- my $db = $self->app->pg->db;
-
- my $res1 = $db->query(
- qq{
- select checkin_station_id
- from journeys
- left join stations on journeys.checkin_station_id = stations.eva
- where stations.eva is null;
- }
- );
-
- my $res2 = $db->query(
- qq{
- select checkout_station_id
- from journeys
- left join stations on journeys.checkout_station_id = stations.eva
- where stations.eva is null;
- }
- );
-
- my %notified;
- while ( my $row = $res1->hash ) {
- my $eva = $row->{checkin_station_id};
- if ( not $found ) {
- $found = 1;
- say
+ my ( $self, $mode ) = @_;
+ my $found = 0;
+ my $db = $self->app->pg->db;
+
+ if ( $mode eq 'all' or $mode eq 'unknown-evas' ) {
+
+ my %notified;
+ my $res1 = $db->query(
+ qq{
+ select checkin_station_id
+ from journeys
+ left join stations on journeys.checkin_station_id = stations.eva
+ where stations.eva is null;
+ }
+ );
+ my $res2 = $db->query(
+ qq{
+ select checkout_station_id
+ from journeys
+ left join stations on journeys.checkout_station_id = stations.eva
+ where stations.eva is null;
+ }
+ );
+
+ while ( my $row = $res1->hash ) {
+ my $eva = $row->{checkin_station_id};
+ if ( not $found ) {
+ $found = 1;
+ say
'Journeys in the travelynx database contain the following unknown EVA IDs.';
- say '------------8<----------';
- say 'Travel::Status::DE::IRIS v'
- . $Travel::Status::DE::IRIS::Stations::VERSION;
- }
- if ( not $notified{$eva} ) {
- say $eva;
- $notified{$eva} = 1;
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ }
+ if ( not $notified{$eva} ) {
+ say $eva;
+ $notified{$eva} = 1;
+ }
}
- }
- while ( my $row = $res2->hash ) {
- my $eva = $row->{checkout_station_id};
- if ( not $found ) {
- $found = 1;
- say
+ while ( my $row = $res2->hash ) {
+ my $eva = $row->{checkout_station_id};
+ if ( not $found ) {
+ $found = 1;
+ say
'Journeys in the travelynx database contain the following unknown EVA IDs.';
- say '------------8<----------';
- say 'Travel::Status::DE::IRIS v'
- . $Travel::Status::DE::IRIS::Stations::VERSION;
- }
- if ( not $notified{$eva} ) {
- say $eva;
- $notified{$eva} = 1;
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ }
+ if ( not $notified{$eva} ) {
+ say $eva;
+ $notified{$eva} = 1;
+ }
}
}
@@ -70,42 +72,101 @@ sub run {
$found = 0;
}
- my $rename = $self->app->renamed_station;
+ if ( $mode eq 'all' or $mode eq 'unknown-route-entries' ) {
- my $res = $db->select( 'journeys', [ 'route', 'edited' ] )->expand;
- while ( my $j = $res->hash ) {
- if ( $j->{edited} & 0x0010 ) {
- next;
- }
- my @stops = @{ $j->{route} // [] };
- for my $stop (@stops) {
- my $stop_name = $stop->[0];
- if ( $rename->{ $stop->[0] } ) {
- $stop->[0] = $rename->{ $stop->[0] };
+ my %notified;
+ my $rename = $self->app->renamed_station;
+ my $res = $db->select( 'journeys', [ 'route', 'edited' ] )->expand;
+
+ while ( my $j = $res->hash ) {
+ if ( $j->{edited} & 0x0010 ) {
+ next;
+ }
+ my @stops = @{ $j->{route} // [] };
+ for my $stop (@stops) {
+ my $stop_name = $stop->[0];
+ if ( $rename->{ $stop->[0] } ) {
+ $stop->[0] = $rename->{ $stop->[0] };
+ }
+ }
+ my @unknown
+ = $self->app->stations->grep_unknown( map { $_->[0] } @stops );
+ for my $stop_name (@unknown) {
+ if ( not $notified{$stop_name} ) {
+ if ( not $found ) {
+ say
+'Journeys in the travelynx database contain the following unknown route entries.';
+ say
+ 'Note that this check ignores manual route entries.';
+ say
+'All reports refer to routes obtained via HAFAS/IRIS.';
+ say '------------8<----------';
+ say 'Travel::Status::DE::IRIS v'
+ . $Travel::Status::DE::IRIS::Stations::VERSION;
+ $found = 1;
+ }
+ say $stop_name;
+ $notified{$stop_name} = 1;
+ }
}
}
- my @unknown
- = $self->app->stations->grep_unknown( map { $_->[0] } @stops );
- for my $stop_name (@unknown) {
- if ( not $notified{$stop_name} ) {
+ }
+
+ if ($found) {
+ say '------------8<----------';
+ say '';
+ $found = 0;
+ }
+
+ if ( $mode eq 'all' or $mode eq 'checkout-eva-vs-route-eva' ) {
+
+ my $res = $db->select(
+ 'journeys_str',
+ [ 'journey_id', 'sched_arr_ts', 'route', 'arr_name', 'arr_eva' ],
+ { backend_id => 0 }
+ )->expand;
+
+ journey: while ( my $j = $res->hash ) {
+ my $found_in_route;
+ my $found_arr;
+ for my $stop ( @{ $j->{route} // [] } ) {
+ if ( not $stop->[1] ) {
+ next journey;
+ }
+ if ( $stop->[1] == $j->{arr_eva} ) {
+ $found_in_route = 1;
+ last;
+ }
+ if ( $stop->[2]{sched_arr}
+ and $j->{sched_arr_ts}
+ and $stop->[2]{sched_arr} == int( $j->{sched_arr_ts} ) )
+ {
+ $found_arr = $stop;
+ }
+ }
+ if ( $found_arr and not $found_in_route ) {
if ( not $found ) {
+ say q{};
say
-'Journeys in the travelynx database contain the following unknown route entries.';
- say 'Note that this check ignores manual route entries.';
- say 'All reports refer to routes obtained via HAFAS/IRIS.';
+'The following journeys have route entries which do not agree with checkout EVA ID.';
+ say
+'checkout station ID (left) vs route entry with matching checkout time (right)';
say '------------8<----------';
- say 'Travel::Status::DE::IRIS v'
- . $Travel::Status::DE::IRIS::Stations::VERSION;
$found = 1;
}
- say $stop_name;
- $notified{$stop_name} = 1;
+ printf(
+ "%7d %d (%s) vs %d (%s)\n",
+ $j->{journey_id}, $j->{arr_eva}, $j->{arr_name},
+ $found_arr->[1], $found_arr->[0]
+ );
}
}
}
+
if ($found) {
say '------------8<----------';
say '';
+ $found = 0;
}
}
diff --git a/lib/Travelynx/Command/maintenance.pm b/lib/Travelynx/Command/maintenance.pm
index c9c7ed6..7baf762 100644
--- a/lib/Travelynx/Command/maintenance.pm
+++ b/lib/Travelynx/Command/maintenance.pm
@@ -153,22 +153,6 @@ sub run {
}
$tx->commit;
-
- # Computing stats may take a while, but we've got all time in the
- # world here. This means users won't have to wait when loading their
- # own journey log.
- say 'Generating missing stats ...';
- for
- my $user ( $db->select( 'users', ['id'], { status => 1 } )->hashes->each )
- {
- $tx = $db->begin;
- $self->app->journeys->generate_missing_stats( uid => $user->{id} );
- $self->app->journeys->get_stats(
- uid => $user->{id},
- year => $now->year
- );
- $tx->commit;
- }
}
1;
diff --git a/lib/Travelynx/Command/traewelling.pm b/lib/Travelynx/Command/traewelling.pm
index 4c47e84..e4e0134 100644
--- a/lib/Travelynx/Command/traewelling.pm
+++ b/lib/Travelynx/Command/traewelling.pm
@@ -20,6 +20,12 @@ sub pull_sync {
my $request_count = 0;
for my $account_data ( $self->app->traewelling->get_pull_accounts ) {
+ if ( -e 'maintenance' ) {
+ $self->app->log->debug(
+ 'treawelling: "maintenance" file found, aborting');
+ return;
+ }
+
my $in_transit = $self->app->in_transit->get(
uid => $account_data->{user_id},
);
@@ -30,6 +36,13 @@ sub pull_sync {
next;
}
+ if ( not defined $account_data->{data}{user_name} ) {
+ $self->app->log->debug(
+"travelynx user $account_data->{user_id} has a Traewellig connection, but no username"
+ );
+ next;
+ }
+
# $account_data->{user_id} is the travelynx uid
# $account_data->{user_name} is the Träwelling username
$request_count += 1;
@@ -39,7 +52,7 @@ sub pull_sync {
# In 'work', the event loop is not running,
# so there's no need to multiply by $request_count at the moment
- Mojo::Promise->timer(1)->then(
+ Mojo::Promise->timer(1.5)->then(
sub {
return $self->app->traewelling_api->get_status_p(
username => $account_data->{data}{user_name},
@@ -77,6 +90,13 @@ sub push_sync {
my %push_result;
for my $candidate ( $self->app->traewelling->get_pushable_accounts ) {
+
+ if ( -e 'maintenance' ) {
+ $self->app->log->debug(
+ 'treawelling: "maintenance" file found, aborting');
+ return;
+ }
+
$self->app->log->debug(
"Pushing to Traewelling for UID $candidate->{uid}");
my $trip_id = $candidate->{journey_data}{trip_id};
@@ -102,12 +122,12 @@ sub push_sync {
my ($status) = @_;
$push_result{ $status->{http} } += 1;
}
- )->catch(
+ )->catch(
sub {
my ($status) = @_;
$push_result{ $status->{http} // 0 } += 1;
}
- )->wait;
+ )->wait;
}
return \%push_result;
@@ -121,6 +141,12 @@ sub run {
my $push_result;
my $pull_result;
+ if ( -e 'maintenance' ) {
+ $self->app->log->debug(
+ 'treawelling: "maintenance" file found, aborting');
+ return;
+ }
+
if ( not $direction or $direction eq 'push' ) {
$push_result = $self->push_sync;
}
@@ -133,6 +159,12 @@ sub run {
my $trwl_pull_finished_at = DateTime->now( time_zone => 'Europe/Berlin' );
+ if ( -e 'maintenance' ) {
+ $self->app->log->debug(
+ 'treawelling: "maintenance" file found, aborting');
+ return;
+ }
+
my $trwl_push_duration = $trwl_push_finished_at->epoch - $started_at->epoch;
my $trwl_pull_duration
= $trwl_pull_finished_at->epoch - $trwl_push_finished_at->epoch;
diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm
index 10b1b69..071befa 100644
--- a/lib/Travelynx/Command/work.pm
+++ b/lib/Travelynx/Command/work.pm
@@ -1,11 +1,14 @@
package Travelynx::Command::work;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Command';
use Mojo::Promise;
+use utf8;
+
use DateTime;
use JSON;
use List::Util;
@@ -15,12 +18,17 @@ has description => 'Update real-time data of active journeys';
has usage => sub { shift->extract_usage };
sub run {
- my ($self) = @_;
+ my ( $self, $backend ) = @_;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
my $checkin_deadline = $now->clone->subtract( hours => 48 );
my $json = JSON->new;
+ if ( -e 'maintenance' ) {
+ $self->app->log->debug('work: "maintenance" file found, aborting');
+ return;
+ }
+
my $num_incomplete = $self->app->in_transit->delete_incomplete_checkins(
earlier_than => $checkin_deadline );
@@ -28,245 +36,700 @@ sub run {
$self->app->log->debug("Removed ${num_incomplete} incomplete checkins");
}
- my $errors = 0;
+ my $errors = 0;
+ my $backend_issues = 0;
+ my $rate_limit_counts = 0;
+ my $dbris_rate_limited = 0;
for my $entry ( $self->app->in_transit->get_all_active ) {
+ if ( -e 'maintenance' ) {
+ $self->app->log->debug('work: "maintenance" file found, aborting');
+ return;
+ }
+
my $uid = $entry->{user_id};
my $dep = $entry->{dep_eva};
my $arr = $entry->{arr_eva};
my $train_id = $entry->{train_id};
- if ( $train_id =~ m{[|]} ) {
+ if ( $train_id eq 'manual'
+ and ( not $backend or $backend eq 'manual' ) )
+ {
+ if ( $arr
+ and $entry->{real_arr_ts}
+ and $now->epoch - $entry->{real_arr_ts} > 600 )
+ {
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ }
+
+ elsif ( $entry->{is_dbris} and ( not $backend or $backend eq 'dbris' ) )
+ {
- $self->app->hafas->get_journey_p( trip_id => $train_id )->then(
- sub {
- my ($journey) = @_;
+ eval {
- my $found_dep;
- my $found_arr;
- for my $stop ( $journey->route ) {
- if ( $stop->loc->eva == $dep ) {
- $found_dep = $stop;
+ Mojo::Promise->timer(
+ $dbris_rate_limited ? 4.5 : ( $backend ? 1.2 : 1.0 ) )
+ ->then(
+ sub {
+ return $self->app->dbris->get_journey_p(
+ trip_id => $train_id );
+ }
+ )->then(
+ sub {
+ my ($journey) = @_;
+
+ $dbris_rate_limited = 0;
+
+ my $found_dep;
+ my $found_arr;
+ for my $stop ( $journey->route ) {
+ if ( $stop->eva == $dep ) {
+ $found_dep = $stop;
+ }
+ if ( $arr and $stop->eva == $arr ) {
+ $found_arr = $stop;
+ last;
+ }
+ }
+ if ( not $found_dep ) {
+ $self->app->log->debug(
+ "Did not find $dep within journey $train_id");
+ return;
}
- if ( $arr and $stop->loc->eva == $arr ) {
- $found_arr = $stop;
- last;
+
+ if ( $found_dep->rt_dep ) {
+ $self->app->in_transit->update_departure_dbris(
+ uid => $uid,
+ journey => $journey,
+ stop => $found_dep,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ train_id => $train_id,
+ );
+ }
+ if ( $found_dep->sched_dep
+ and $found_dep->dep->epoch > $now->epoch )
+ {
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $train_id,
+ is_departure => 1,
+ eva => $dep,
+ datetime => $found_dep->sched_dep,
+ train_type => $journey->type,
+ train_no => $journey->number,
+ );
+ $self->app->add_stationinfo( $uid, 1,
+ $train_id, $found_dep->eva );
+ }
+
+ if (
+ $found_arr
+ and
+ ( $found_arr->rt_arr or $found_arr->is_cancelled )
+ )
+ {
+ $self->app->in_transit->update_arrival_dbris(
+ uid => $uid,
+ journey => $journey,
+ train_id => $train_id,
+ stop => $found_arr,
+ dep_eva => $dep,
+ arr_eva => $arr
+ );
+ }
+ if ( $found_arr and $found_arr->rt_arr ) {
+ if ( $found_arr->arr->epoch - $now->epoch < 600 ) {
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $train_id,
+ is_arrival => 1,
+ eva => $arr,
+ datetime => $found_arr->sched_dep,
+ train_type => $journey->type,
+ train_no => $journey->number,
+ );
+ $self->app->add_stationinfo( $uid, 0,
+ $train_id, $found_dep->eva,
+ $found_arr->eva );
+ }
+ }
+ if ( $found_arr and $found_arr->is_cancelled ) {
+
+ # check out (adds a cancelled journey and resets journey state
+ # to destination selection)
+ $self->app->checkout_p(
+ station => $arr,
+ force => 0,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
}
}
- if ( not $found_dep ) {
+ )->catch(
+ sub {
+ my ($err) = @_;
$self->app->log->debug(
- "Did not find $dep within journey $train_id");
- return;
- }
-
- if ( $found_dep->{rt_dep} ) {
- $self->app->in_transit->update_departure_hafas(
- uid => $uid,
- journey => $journey,
- stop => $found_dep,
- dep_eva => $dep,
- arr_eva => $arr
+"work($uid) @ DBRIS $entry->{backend_name}: journey: $err"
);
+ if ( $err =~ m{HTTP 429} ) {
+ $dbris_rate_limited = 1;
+ $rate_limit_counts += 1;
+ }
+ else {
+ $backend_issues += 1;
+ }
}
+ )->wait;
- if ( $found_arr and $found_arr->{rt_arr} ) {
- $self->app->in_transit->update_arrival_hafas(
- uid => $uid,
- journey => $journey,
- stop => $found_arr,
- dep_eva => $dep,
- arr_eva => $arr
+ if ( $arr
+ and $entry->{real_arr_ts}
+ and $now->epoch - $entry->{real_arr_ts} > 600 )
+ {
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ };
+ if ($@) {
+ $errors += 1;
+ $self->app->log->error(
+ "work($uid) @ DBRIS $entry->{backend_name}: $@");
+ }
+ }
+
+ elsif ( $entry->{is_efa} and ( not $backend or $backend eq 'efa' ) ) {
+ eval {
+ $self->app->efa->get_journey_p(
+ trip_id => $train_id,
+ service => $entry->{backend_name}
+ )->then(
+ sub {
+ my ($journey) = @_;
+
+ my $found_dep;
+ my $found_arr;
+ for my $stop ( $journey->route ) {
+ if ( $stop->id_num == $dep ) {
+ $found_dep = $stop;
+ }
+ if ( $arr and $stop->id_num == $arr ) {
+ $found_arr = $stop;
+ last;
+ }
+ }
+ if ( not $found_dep ) {
+ $self->app->log->debug(
+ "Did not find $dep within journey $train_id");
+ return;
+ }
+
+ if ( $found_dep->rt_dep ) {
+ $self->app->in_transit->update_departure_efa(
+ uid => $uid,
+ journey => $journey,
+ stop => $found_dep,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ trip_id => $train_id,
+ );
+ }
+
+ if (
+ $found_arr
+ and
+ ( $found_arr->rt_arr or $found_arr->is_cancelled )
+ )
+ {
+ $self->app->in_transit->update_arrival_efa(
+ uid => $uid,
+ journey => $journey,
+ stop => $found_arr,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ trip_id => $train_id,
+ );
+ }
+ if ( $found_arr and $found_arr->is_cancelled ) {
+
+ # check out (adds a cancelled journey and resets journey state
+ # to destination selection)
+ $self->app->checkout_p(
+ station => $arr,
+ force => 0,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $backend_issues += 1;
+ $self->app->log->error(
+"work($uid) @ EFA $entry->{backend_name}: journey: $err"
);
}
+ )->wait;
+
+ if ( $arr
+ and $entry->{real_arr_ts}
+ and $now->epoch - $entry->{real_arr_ts} > 600 )
+ {
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
}
- )->catch(
- sub {
- my ($err) = @_;
- if ( $err =~ m{svcResL\[0\][.]err is (?:FAIL|PARAMETER)$} )
- {
- # HAFAS do be weird. These are not actionable.
- $self->app->log->debug("work($uid)/journey: $err");
+ };
+ if ($@) {
+ $errors += 1;
+ $self->app->log->error(
+ "work($uid) @ EFA $entry->{backend_name}: $@");
+ }
+ }
+
+ elsif ( $entry->{is_motis} and ( not $backend or $backend eq 'motis' ) )
+ {
+
+ eval {
+ $self->app->motis->get_trip_p(
+ service => $entry->{backend_name},
+ trip_id => $train_id,
+ )->then(
+ sub {
+ my ($journey) = @_;
+
+ for my $stopover ( $journey->stopovers ) {
+ if ( not defined $stopover->stop->{eva} ) {
+
+ # Looks like MOTIS / transitous station IDs can change after the fact.
+ # So let's be safe rather than sorry, even if this causes way too many calls to the slow path
+ # (Stations::get_by_external_id uses string lookups).
+ # This function call implicitly sets $stopover->stop->{eva} for MOTIS backends.
+ $self->app->stations->add_or_update(
+ stop => $stopover->stop,
+ motis => $entry->{backend_name},
+ );
+
+ $self->app->log->debug( "mapped "
+ . $stopover->stop->id . " to "
+ . $stopover->stop->{eva} );
+ }
+ }
+
+ my $found_departure;
+ my $found_arrival;
+ for my $stopover ( $journey->stopovers ) {
+ if ( $stopover->stop->{eva} == $dep ) {
+ $found_departure = $stopover;
+ }
+
+ if ( $arr and $stopover->stop->{eva} == $arr ) {
+ $found_arrival = $stopover;
+ last;
+ }
+ }
+
+ if ( not $found_departure ) {
+ $self->app->log->debug(
+ "Did not find $dep within trip $train_id");
+ return;
+ }
+
+ if ( $found_departure->realtime_departure ) {
+ $self->app->in_transit->update_departure_motis(
+ uid => $uid,
+ journey => $journey,
+ stopover => $found_departure,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ train_id => $train_id,
+ );
+ }
+
+ if ( $found_arrival
+ and $found_arrival->realtime_arrival )
+ {
+ $self->app->in_transit->update_arrival_motis(
+ uid => $uid,
+ journey => $journey,
+ train_id => $train_id,
+ stopover => $found_arrival,
+ dep_eva => $dep,
+ arr_eva => $arr
+ );
+ }
}
- else {
- $self->app->log->error("work($uid)/journey: $err");
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->app->log->error(
+"work($uid) @ MOTIS $entry->{backend_name}: journey: $err"
+ );
}
+ )->wait;
+
+ if ( $arr
+ and $entry->{real_arr_ts}
+ and $now->epoch - $entry->{real_arr_ts} > 600 )
+ {
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
}
- )->wait;
+ };
+ if ($@) {
+ $errors += 1;
+ $self->app->log->error(
+ "work($uid) @ MOTIS $entry->{backend_name}: $@");
+ }
+ }
- if ( $arr
- and $entry->{real_arr_ts}
- and $now->epoch - $entry->{real_arr_ts} > 600 )
- {
- $self->app->checkout_p(
- station => $arr,
- force => 2,
- dep_eva => $dep,
- arr_eva => $arr,
- uid => $uid
+ elsif ( $entry->{is_hafas} and ( not $backend or $backend eq 'hafas' ) )
+ {
+
+ eval {
+
+ $self->app->hafas->get_journey_p(
+ trip_id => $train_id,
+ service => $entry->{backend_name}
+ )->then(
+ sub {
+ my ($journey) = @_;
+
+ my $found_dep;
+ my $found_arr;
+ for my $stop ( $journey->route ) {
+ if ( $stop->loc->eva == $dep ) {
+ $found_dep = $stop;
+ }
+ if ( $arr and $stop->loc->eva == $arr ) {
+ $found_arr = $stop;
+ last;
+ }
+ }
+ if ( not $found_dep ) {
+ $self->app->log->debug(
+ "Did not find $dep within journey $train_id");
+ return;
+ }
+
+ if ( $found_dep->rt_dep ) {
+ $self->app->in_transit->update_departure_hafas(
+ uid => $uid,
+ journey => $journey,
+ stop => $found_dep,
+ dep_eva => $dep,
+ arr_eva => $arr
+ );
+ }
+ if (
+ $found_dep->sched_dep
+ and ( $entry->{backend_id} <= 1
+ or $entry->{backend_name} eq 'VRN'
+ or $entry->{backend_name} eq 'ÖBB' )
+ and $journey->class <= 16
+ and $found_dep->dep->epoch > $now->epoch
+ )
+ {
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $journey->id,
+ is_departure => 1,
+ eva => $dep,
+ datetime => $found_dep->sched_dep,
+ train_type => $journey->type =~ s{ +$}{}r,
+ train_no => $journey->number,
+ );
+ $self->app->add_stationinfo( $uid, 1,
+ $journey->id, $found_dep->loc->eva );
+ }
+
+ if ( $found_arr and $found_arr->rt_arr ) {
+ $self->app->in_transit->update_arrival_hafas(
+ uid => $uid,
+ journey => $journey,
+ stop => $found_arr,
+ dep_eva => $dep,
+ arr_eva => $arr
+ );
+ if (
+ (
+ $entry->{backend_id} <= 1
+ or $entry->{backend_name} eq 'VRN'
+ or $entry->{backend_name} eq 'ÖBB'
+ )
+ and $journey->class <= 16
+ and $found_arr->arr->epoch - $now->epoch < 600
+ )
+ {
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $journey->id,
+ is_arrival => 1,
+ eva => $arr,
+ datetime => $found_arr->sched_dep,
+ train_type => $journey->type,
+ train_no => $journey->number,
+ );
+ $self->app->add_stationinfo( $uid, 0,
+ $journey->id, $found_dep->loc->eva,
+ $found_arr->loc->eva );
+ }
+ }
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $backend_issues += 1;
+ if ( $err
+ =~ m{svcResL\[0\][.]err is (?:FAIL|PARAMETER)$}
+ or $err =~ m{timeout} )
+ {
+ # These are not actionable.
+ $self->app->log->debug(
+"work($uid) @ HAFAS $entry->{backend_name}: journey: $err"
+ );
+ }
+ else {
+ $self->app->log->error(
+"work($uid) @ HAFAS $entry->{backend_name}: journey: $err"
+ );
+ }
+ }
)->wait;
+
+ if ( $arr
+ and $entry->{real_arr_ts}
+ and $now->epoch - $entry->{real_arr_ts} > 600 )
+ {
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ };
+ if ($@) {
+ $errors += 1;
+ $self->app->log->error(
+ "work($uid) @ HAFAS $entry->{backend_name}: $@");
}
- next;
}
+ # TODO irgendwo ist hier ne race condition wo ein neuer checkin (in HAFAS) mit IRIS-Daten überschrieben wird.
+ # Die ganzen updates brauchen wirklich mal sanity checks mit train id ...
+
# Note: IRIS data is not always updated in real-time. Both departure and
# arrival delays may take several minutes to appear, especially in case
# of large-scale disturbances. We work around this by continuing to
# update departure data for up to 15 minutes after departure and
# delaying automatic checkout by at least 10 minutes.
- eval {
- if ( $now->epoch - $entry->{real_dep_ts} < 900 ) {
- my $status = $self->app->iris->get_departures(
- station => $dep,
- lookbehind => 30,
- lookahead => 30
- );
- if ( $status->{errstr} ) {
- die("get_departures($dep): $status->{errstr}\n");
- }
-
- my ($train) = List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
+ elsif ( $entry->{is_iris} and ( not $backend or $backend eq 'iris' ) ) {
+ eval {
+ if ( $now->epoch - $entry->{real_dep_ts} < 900 ) {
+ my $status = $self->app->iris->get_departures(
+ station => $dep,
+ lookbehind => 30,
+ lookahead => 30
+ );
+ if ( $status->{errstr} ) {
+ die("get_departures($dep): $status->{errstr}\n");
+ }
- if ( not $train ) {
- $self->app->log->debug(
- "could not find train $train_id at $dep\n");
- return;
- }
+ my ($train)
+ = List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- $self->app->in_transit->update_departure(
- uid => $uid,
- train => $train,
- dep_eva => $dep,
- arr_eva => $arr,
- route => [ $self->app->iris->route_diff($train) ]
- );
+ if ( not $train ) {
+ $self->app->log->debug(
+ "could not find train $train_id at $dep\n");
+ return;
+ }
- if ( $train->departure_is_cancelled and $arr ) {
- my $checked_in
- = $self->app->in_transit->update_departure_cancelled(
+ $self->app->in_transit->update_departure(
uid => $uid,
train => $train,
dep_eva => $dep,
arr_eva => $arr,
- );
-
- # depending on the amount of users in transit, some time may
- # have passed between fetching $entry from the database and
- # now. Only check out if the user is still checked into this
- # train.
- if ($checked_in) {
+ route => [ $self->app->iris->route_diff($train) ]
+ );
- # check out (adds a cancelled journey and resets journey state
- # to checkin
- $self->app->checkout_p(
- station => $arr,
- force => 2,
+ if ( $train->departure_is_cancelled and $arr ) {
+ my $checked_in
+ = $self->app->in_transit->update_departure_cancelled(
+ uid => $uid,
+ train => $train,
dep_eva => $dep,
arr_eva => $arr,
- uid => $uid
- )->wait;
+ );
+
+ # depending on the amount of users in transit, some time may
+ # have passed between fetching $entry from the database and
+ # now. Only check out if the user is still checked into this
+ # train.
+ if ($checked_in) {
+
+ # check out (adds a cancelled journey and resets journey state
+ # to checkin
+ $self->app->checkout_p(
+ station => $arr,
+ force => 2,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ }
+ else {
+ $self->app->add_route_timestamps( $uid, $train, 1 );
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $train->train_id,
+ is_departure => 1,
+ eva => $dep,
+ datetime => $train->sched_departure,
+ train_type => $train->type,
+ train_no => $train->train_no
+ );
+ $self->app->add_stationinfo( $uid, 1, $train->train_id,
+ $dep, $arr );
}
}
- else {
- $self->app->add_route_timestamps( $uid, $train, 1 );
- }
+ };
+ if ($@) {
+ $errors += 1;
+ $self->app->log->error("work($uid) @ IRIS: departure: $@");
}
- };
- if ($@) {
- $errors += 1;
- $self->app->log->error("work($uid)/departure: $@");
- }
- eval {
- if (
- $arr
- and ( not $entry->{real_arr_ts}
- or $now->epoch - $entry->{real_arr_ts} < 600 )
- )
- {
- my $status = $self->app->iris->get_departures(
- station => $arr,
- lookbehind => 20,
- lookahead => 220
- );
- if ( $status->{errstr} ) {
- die("get_departures($arr): $status->{errstr}\n");
- }
+ eval {
+ if (
+ $arr
+ and ( not $entry->{real_arr_ts}
+ or $now->epoch - $entry->{real_arr_ts} < 600 )
+ )
+ {
+ my $status = $self->app->iris->get_departures(
+ station => $arr,
+ lookbehind => 20,
+ lookahead => 220
+ );
+ if ( $status->{errstr} ) {
+ die("get_departures($arr): $status->{errstr}\n");
+ }
- # Note that a train may pass the same station several times.
- # Notable example: S41 / S42 ("Ringbahn") both starts and
- # terminates at Berlin Südkreuz
- my ($train) = List::Util::first {
- $_->train_id eq $train_id
- and $_->sched_arrival
- and $_->sched_arrival->epoch > $entry->{sched_dep_ts}
- }
- @{ $status->{results} };
+ # Note that a train may pass the same station several times.
+ # Notable example: S41 / S42 ("Ringbahn") both starts and
+ # terminates at Berlin Südkreuz
+ my ($train) = List::Util::first {
+ $_->train_id eq $train_id
+ and $_->sched_arrival
+ and $_->sched_arrival->epoch > $entry->{sched_dep_ts}
+ }
+ @{ $status->{results} };
- $train //= List::Util::first { $_->train_id eq $train_id }
- @{ $status->{results} };
+ $train //= List::Util::first { $_->train_id eq $train_id }
+ @{ $status->{results} };
- if ( not $train ) {
+ if ( not $train ) {
- # If we haven't seen the train yet, its arrival is probably
- # too far in the future. This is not critical.
- return;
- }
+ # If we haven't seen the train yet, its arrival is probably
+ # too far in the future. This is not critical.
+ return;
+ }
- my $checked_in = $self->app->in_transit->update_arrival(
- uid => $uid,
- train => $train,
- route => [ $self->app->iris->route_diff($train) ],
- dep_eva => $dep,
- arr_eva => $arr,
- );
+ my $checked_in = $self->app->in_transit->update_arrival(
+ uid => $uid,
+ train => $train,
+ route => [ $self->app->iris->route_diff($train) ],
+ dep_eva => $dep,
+ arr_eva => $arr,
+ );
- if ( $checked_in and $train->arrival_is_cancelled ) {
+ if ( $checked_in and $train->arrival_is_cancelled ) {
- # check out (adds a cancelled journey and resets journey state
- # to destination selection)
- $self->app->checkout_p(
+ # check out (adds a cancelled journey and resets journey state
+ # to destination selection)
+ $self->app->checkout_p(
+ station => $arr,
+ force => 0,
+ dep_eva => $dep,
+ arr_eva => $arr,
+ uid => $uid
+ )->wait;
+ }
+ else {
+ $self->app->add_route_timestamps(
+ $uid, $train, 0,
+ (
+ defined $entry->{real_arr_ts}
+ and $now->epoch > $entry->{real_arr_ts}
+ ) ? 1 : 0
+ );
+ $self->app->add_wagonorder(
+ uid => $uid,
+ train_id => $train->train_id,
+ is_arrival => 1,
+ eva => $arr,
+ datetime => $train->sched_departure,
+ train_type => $train->type,
+ train_no => $train->train_no
+ );
+ $self->app->add_stationinfo( $uid, 0, $train->train_id,
+ $dep, $arr );
+ }
+ }
+ elsif ( $entry->{real_arr_ts} ) {
+ my ( undef, $error ) = $self->app->checkout_p(
station => $arr,
- force => 0,
+ force => 2,
dep_eva => $dep,
arr_eva => $arr,
uid => $uid
+ )->catch(
+ sub {
+ my ($error) = @_;
+ $backend_issues += 1;
+ $self->app->log->error(
+ "work($uid) @ IRIS: arrival: $error");
+ $errors += 1;
+ }
)->wait;
}
- else {
- $self->app->add_route_timestamps(
- $uid, $train, 0,
- (
- defined $entry->{real_arr_ts}
- and $now->epoch > $entry->{real_arr_ts}
- ) ? 1 : 0
- );
- }
+ };
+ if ($@) {
+ $self->app->log->error("work($uid) @ IRIS: arrival: $@");
+ $errors += 1;
}
- elsif ( $entry->{real_arr_ts} ) {
- my ( undef, $error ) = $self->app->checkout_p(
- station => $arr,
- force => 2,
- dep_eva => $dep,
- arr_eva => $arr,
- uid => $uid
- )->catch(
- sub {
- my ($error) = @_;
- $self->app->log->error("work($uid)/arrival: $error");
- $errors += 1;
- }
- )->wait;
- }
- };
- if ($@) {
- $self->app->log->error("work($uid)/arrival: $@");
- $errors += 1;
+
+ eval { };
}
- eval { };
}
my $started_at = $now;
@@ -274,22 +737,35 @@ sub run {
my $worker_duration = $main_finished_at->epoch - $started_at->epoch;
if ( $self->app->config->{influxdb}->{url} ) {
+ my $tags = q{};
+ if ($backend) {
+ $tags .= ",backend=${backend}";
+ }
if ( $self->app->mode eq 'development' ) {
$self->app->log->debug( 'POST '
. $self->app->config->{influxdb}->{url}
- . " worker runtime_seconds=${worker_duration},errors=${errors}"
+ . " worker${tags} runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}"
);
}
else {
$self->app->ua->post_p( $self->app->config->{influxdb}->{url},
- "worker runtime_seconds=${worker_duration},errors=${errors}" )
- ->wait;
+"worker${tags} runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}"
+ )->wait;
}
}
if ( not $self->app->config->{traewelling}->{separate_worker} ) {
$self->app->start('traewelling');
}
+
+ # add_wagonorder and add_stationinfo assume a permanently running IOLoop
+ # and do not allow Mojolicious commands to wait until they have completed.
+ # Hence, some add_wagonorder and add_stationinfo calls made here may not
+ # complete before the work command exits, and thus have no effect.
+ #
+ # This is not ideal and will need fixing at some point. Until then, here
+ # is the pragmatic solution for 99% of the associated issues.
+ Mojo::Promise->timer(5)->wait;
}
1;
diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm
index f1dc43e..bf1eac2 100644
--- a/lib/Travelynx/Controller/Account.pm
+++ b/lib/Travelynx/Controller/Account.pm
@@ -1,11 +1,13 @@
package Travelynx::Controller::Account;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
use JSON;
+use Math::Polygon;
use Mojo::Util qw(xml_escape);
use Text::Markdown;
use UUID::Tiny qw(:std);
@@ -45,6 +47,7 @@ sub send_registration_mail {
my $ua = $self->req->headers->user_agent;
my $reg_url = $self->url_for('reg')->to_abs->scheme('https');
+ my $tos_url = $self->url_for('tos')->to_abs->scheme('https');
my $imprint_url = $self->url_for('impressum')->to_abs->scheme('https');
my $body = "Hallo, ${user}!\n\n";
@@ -53,7 +56,8 @@ sub send_registration_mail {
$body
.= "Falls die Registrierung von dir ausging, kannst du den Account unter\n";
$body .= "${reg_url}/${user_id}/${token}\n";
- $body .= "freischalten.\n\n";
+ $body .= "freischalten.\n";
+ $body .= "Beachte dabei die Nutzungsbedingungen: ${tos_url}\n\n";
$body
.= "Falls nicht, ignoriere diese Mail bitte. Nach etwa 48 Stunden wird deine\n";
$body
@@ -831,29 +835,6 @@ sub insight {
}
-sub services {
- my ($self) = @_;
- my $user = $self->current_user;
-
- if ( $self->param('action') and $self->param('action') eq 'save' ) {
- my $sb = $self->param('stationboard');
- my $value = 0;
- if ( $sb =~ m{ ^ \d+ $ }x and $sb >= 0 and $sb <= 4 ) {
- $value = int($sb);
- }
- $self->users->use_external_services(
- uid => $user->{id},
- set => $value
- );
- $self->flash( success => 'external' );
- $self->redirect_to('account');
- }
-
- $self->param( stationboard =>
- $self->users->use_external_services( uid => $user->{id} ) );
- $self->render('use_external_links');
-}
-
sub webhook {
my ($self) = @_;
@@ -1022,6 +1003,273 @@ sub password_form {
$self->render('change_password');
}
+sub lonlat_in_polygon {
+ my ( $self, $polygon, $lonlat ) = @_;
+
+ my $circle = shift( @{$polygon} );
+ my @holes = @{$polygon};
+
+ my $circle_poly = Math::Polygon->new( @{$circle} );
+ if ( $circle_poly->contains($lonlat) ) {
+ for my $hole (@holes) {
+ my $hole_poly = Math::Polygon->new( @{$hole} );
+ if ( $hole_poly->contains($lonlat) ) {
+ return;
+ }
+ }
+ return 1;
+ }
+ return;
+}
+
+sub backend_form {
+ my ($self) = @_;
+ my $user = $self->current_user;
+
+ my @backends = $self->stations->get_backends;
+ my @suggested_backends;
+
+ 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 ( $user_lat, $user_lon )
+ = $self->journeys->get_latest_checkout_latlon( uid => $user->{id} );
+
+ for my $backend (@backends) {
+ my $type = 'UNKNOWN';
+ if ( $backend->{iris} ) {
+ $type = 'IRIS-TTS';
+ $backend->{name} = 'IRIS';
+ $backend->{longname} = 'Deutsche Bahn: IRIS-TTS';
+ $backend->{homepage} = 'https://www.bahn.de';
+ $backend->{legacy} = 1;
+ }
+ elsif ( $backend->{dbris} ) {
+ $type = 'DBRIS';
+ $backend->{longname} = 'Deutsche Bahn: bahn.de';
+ $backend->{homepage} = 'https://www.bahn.de';
+ $backend->{recommended} = 1;
+ }
+ elsif ( $backend->{efa} ) {
+ if ( my $s = $self->efa->get_service( $backend->{name} ) ) {
+ $type = 'EFA';
+ $backend->{longname} = $s->{name};
+ $backend->{homepage} = $s->{homepage};
+ $backend->{regions} = [ map { $place_map{$_} // $_ }
+ @{ $s->{coverage}{regions} // [] } ];
+ $backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
+ $backend->{association} = 1;
+
+ if (
+ $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'Polygon'
+ and $self->lonlat_in_polygon(
+ $s->{coverage}{area}{coordinates},
+ [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ }
+ elsif ( $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'MultiPolygon' )
+ {
+ for my $s_poly (
+ @{ $s->{coverage}{area}{coordinates} // [] } )
+ {
+ if (
+ $self->lonlat_in_polygon(
+ $s_poly, [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ last;
+ }
+ }
+ }
+ }
+ else {
+ $type = undef;
+ }
+ }
+ elsif ( $backend->{hafas} ) {
+
+ # These backends lack a journey endpoint or are no longer
+ # operational and are thus useless for travelynx
+ if ( $backend->{name} eq 'Resrobot'
+ or $backend->{name} eq 'TPG'
+ or $backend->{name} eq 'VRN'
+ or $backend->{name} eq 'DB' )
+ {
+ $type = undef;
+ }
+
+ # PKP is behind a GeoIP filter. Only list it if travelynx.conf
+ # indicates that our IP is allowed or provides a proxy.
+ elsif (
+ $backend->{name} eq 'PKP'
+ and not( $self->app->config->{hafas}{PKP}{geoip_ok}
+ or $self->app->config->{hafas}{PKP}{proxy} )
+ )
+ {
+ $type = undef;
+ }
+ elsif ( my $s = $self->hafas->get_service( $backend->{name} ) ) {
+ $type = 'HAFAS';
+ $backend->{longname} = $s->{name};
+ $backend->{homepage} = $s->{homepage};
+ $backend->{regions} = [ map { $place_map{$_} // $_ }
+ @{ $s->{coverage}{regions} // [] } ];
+ $backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
+
+ if ( $backend->{name} eq 'ÖBB' ) {
+ $backend->{recommended} = 1;
+ }
+ else {
+ $backend->{association} = 1;
+ }
+
+ if (
+ $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'Polygon'
+ and $self->lonlat_in_polygon(
+ $s->{coverage}{area}{coordinates},
+ [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ }
+ elsif ( $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'MultiPolygon' )
+ {
+ for my $s_poly (
+ @{ $s->{coverage}{area}{coordinates} // [] } )
+ {
+ if (
+ $self->lonlat_in_polygon(
+ $s_poly, [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ last;
+ }
+ }
+ }
+ }
+ else {
+ $type = undef;
+ }
+ }
+ elsif ( $backend->{motis} ) {
+ my $s = $self->motis->get_service( $backend->{name} );
+
+ $type = 'MOTIS';
+ $backend->{longname} = $s->{name};
+ $backend->{homepage} = $s->{homepage};
+ $backend->{regions} = [ map { $place_map{$_} // $_ }
+ @{ $s->{coverage}{regions} // [] } ];
+ $backend->{has_area} = $s->{coverage}{area} ? 1 : 0;
+ $backend->{experimental} = 1;
+
+ if ( $backend->{name} eq 'transitous' ) {
+ $backend->{regions} = ['Weltweit'];
+ }
+ if ( $backend->{name} eq 'RNV' ) {
+ $backend->{homepage} = 'https://rnv-online.de/';
+ }
+
+ if (
+ $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'Polygon'
+ and $self->lonlat_in_polygon(
+ $s->{coverage}{area}{coordinates},
+ [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ }
+ elsif ( $s->{coverage}{area}
+ and $s->{coverage}{area}{type} eq 'MultiPolygon' )
+ {
+ for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } )
+ {
+ if (
+ $self->lonlat_in_polygon(
+ $s_poly, [ $user_lon, $user_lat ]
+ )
+ )
+ {
+ push( @suggested_backends, $backend );
+ last;
+ }
+ }
+ }
+ }
+ $backend->{type} = $type;
+ }
+
+ @backends = map { $_->[1] }
+ sort { $a->[0] cmp $b->[0] }
+ map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends;
+
+ $self->render(
+ 'select_backend',
+ suggestions => \@suggested_backends,
+ backends => \@backends,
+ user => $user,
+ redirect_to => $self->req->param('redirect_to') // '/',
+ );
+}
+
+sub change_backend {
+ my ($self) = @_;
+
+ my $backend_id = $self->req->param('backend');
+ my $redir = $self->req->param('redirect_to') // '/';
+
+ if ( $backend_id !~ m{ ^ \d+ $ }x ) {
+ $self->redirect_to($redir);
+ }
+
+ $self->users->set_backend(
+ uid => $self->current_user->{id},
+ backend_id => $backend_id,
+ );
+
+ $self->redirect_to($redir);
+}
+
sub change_password {
my ($self) = @_;
my $old_password = $self->req->param('oldpw');
diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm
index 687243d..572d3fa 100755
--- a/lib/Travelynx/Controller/Api.pm
+++ b/lib/Travelynx/Controller/Api.pm
@@ -21,6 +21,9 @@ sub sanitize {
if ( not defined $value ) {
return undef;
}
+ if ( not defined $type ) {
+ return $value ? ( '' . $value ) : undef;
+ }
if ( $type eq '' ) {
return '' . $value;
}
@@ -51,6 +54,8 @@ sub documentation {
sub get_v1 {
my ($self) = @_;
+ $self->res->headers->access_control_allow_origin(q{*});
+
my $api_action = $self->stash('user_action');
my $api_token = $self->stash('token');
if ( $api_action !~ qr{ ^ (?: status | history | action ) $ }x ) {
@@ -117,6 +122,7 @@ sub travel_v1 {
deprecated => \0,
error => 'Malformed JSON',
},
+ status => 400,
);
return;
}
@@ -130,6 +136,7 @@ sub travel_v1 {
deprecated => \0,
error => 'Malformed token',
},
+ status => 400,
);
return;
}
@@ -143,6 +150,7 @@ sub travel_v1 {
deprecated => \0,
error => 'Malformed token',
},
+ status => 400,
);
return;
}
@@ -155,6 +163,7 @@ sub travel_v1 {
deprecated => \0,
error => 'Invalid token',
},
+ status => 400,
);
return;
}
@@ -169,6 +178,7 @@ sub travel_v1 {
error => 'Missing or invalid action',
status => $self->get_user_status_json_v1( uid => $uid )
},
+ status => 400,
);
return;
}
@@ -177,7 +187,13 @@ sub travel_v1 {
my $from_station = sanitize( q{}, $payload->{fromStation} );
my $to_station = sanitize( q{}, $payload->{toStation} );
my $train_id;
- my $hafas = exists $payload->{train}{journeyID} ? 1 : 0;
+ my $dbris = sanitize( undef, $payload->{dbris} );
+ my $hafas = sanitize( undef, $payload->{hafas} );
+ my $motis = sanitize( undef, $payload->{motis} );
+
+ if ( not $hafas and exists $payload->{train}{journeyID} ) {
+ $dbris //= 'bahn.de';
+ }
if (
not(
@@ -195,11 +211,15 @@ sub travel_v1 {
error => 'Missing fromStation or train data',
status => $self->get_user_status_json_v1( uid => $uid )
},
+ status => 400,
);
return;
}
- if ( not $hafas and not $self->stations->search($from_station) ) {
+ if ( not $hafas
+ and not $dbris
+ and not $self->stations->search( $from_station, backend_id => 1 ) )
+ {
$self->render(
json => {
success => \0,
@@ -207,13 +227,15 @@ sub travel_v1 {
error => 'Unknown fromStation',
status => $self->get_user_status_json_v1( uid => $uid )
},
+ status => 400,
);
return;
}
if ( $to_station
and not $hafas
- and not $self->stations->search($to_station) )
+ and not $dbris
+ and not $self->stations->search( $to_station, backend_id => 1 ) )
{
$self->render(
json => {
@@ -222,6 +244,7 @@ sub travel_v1 {
error => 'Unknown toStation',
status => $self->get_user_status_json_v1( uid => $uid )
},
+ status => 400,
);
return;
}
@@ -273,7 +296,10 @@ sub travel_v1 {
return $self->checkin_p(
station => $from_station,
train_id => $train_id,
- uid => $uid
+ uid => $uid,
+ hafas => $hafas,
+ dbris => $dbris,
+ motis => $motis,
);
}
)->then(
@@ -518,8 +544,9 @@ sub import_v1 {
$payload->{toStation}{realTime}
// $payload->{toStation}{scheduledTime}
),
- comment => sanitize( q{}, $payload->{comment} ),
- lax => $payload->{lax} ? 1 : 0,
+ comment => sanitize( q{}, $payload->{comment} ),
+ lax => $payload->{lax} ? 1 : 0,
+ backend_id => 1,
);
if ( $payload->{intermediateStops}
@@ -558,14 +585,20 @@ sub import_v1 {
my $journey;
if ( not $error ) {
- $journey = $self->journeys->get_single(
- uid => $uid,
- db => $db,
- journey_id => $journey_id,
- verbose => 1
- );
- $error
- = $self->journeys->sanity_check( $journey, $payload->{lax} ? 1 : 0 );
+ eval {
+ $journey = $self->journeys->get_single(
+ uid => $uid,
+ db => $db,
+ journey_id => $journey_id,
+ verbose => 1
+ );
+ $error
+ = $self->journeys->sanity_check( $journey,
+ $payload->{lax} ? 1 : 0 );
+ };
+ if ($@) {
+ $error = $@;
+ }
}
if ($error) {
@@ -654,10 +687,14 @@ sub autocomplete {
$self->res->headers->cache_control('max-age=86400, immutable');
+ my $backend_id = $self->param('backend_id') // 1;
+
my $output
= "document.addEventListener('DOMContentLoaded',function(){M.Autocomplete.init(document.querySelectorAll('.autocomplete'),{\n";
$output .= 'minLength:3,limit:50,data:';
- $output .= encode_json( $self->stations->get_for_autocomplete );
+ $output
+ .= encode_json(
+ $self->stations->get_for_autocomplete( backend_id => $backend_id ) );
$output .= "\n});});\n";
$self->render(
diff --git a/lib/Travelynx/Controller/Passengerrights.pm b/lib/Travelynx/Controller/Passengerrights.pm
index d80f1ae..5759d2e 100644
--- a/lib/Travelynx/Controller/Passengerrights.pm
+++ b/lib/Travelynx/Controller/Passengerrights.pm
@@ -121,6 +121,8 @@ sub list_candidates {
}
}
+ my @abo_journeys
+ = grep { $_->{delay} >= 20 and $_->{delay} < 60 } @journeys;
@journeys = grep { $_->{delay} >= 60 or $_->{connection_missed} } @journeys;
my @cancelled = $self->journeys->get(
@@ -154,8 +156,9 @@ sub list_candidates {
$self->respond_to(
json => { json => [@journeys] },
any => {
- template => 'passengerrights',
- journeys => [@journeys]
+ template => 'passengerrights',
+ journeys => [@journeys],
+ abo_journeys => [@abo_journeys]
}
);
}
diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm
index a063c10..db30d36 100755
--- a/lib/Travelynx/Controller/Profile.pm
+++ b/lib/Travelynx/Controller/Profile.pm
@@ -111,6 +111,13 @@ sub profile {
$status->{arr_name} = undef;
}
+ my $map_data = {};
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
+
my @journeys;
if (
@@ -152,34 +159,47 @@ sub profile {
@journeys = $self->journeys->get(%opt);
}
- $self->render(
- 'profile',
- title => "travelynx: $name",
- name => $name,
- uid => $user->{id},
- privacy => $user,
- bio => $profile->{bio}{html},
- metadata => $profile->{metadata},
- is_self => $is_self,
- following => ( $relation and $relation eq 'follows' ) ? 1 : 0,
- follow_requested => ( $relation and $relation eq 'requests_follow' )
- ? 1
- : 0,
- can_follow => ( $my_user and $user->{accept_follows} and not $relation )
- ? 1
- : 0,
- can_request_follow =>
- ( $my_user and $user->{accept_follow_requests} and not $relation )
- ? 1
- : 0,
- follows_me => ( $inverse_relation and $inverse_relation eq 'follows' )
- ? 1
- : 0,
- follow_reqs_me =>
- ( $inverse_relation and $inverse_relation eq 'requests_follow' ) ? 1
- : 0,
- journey => $status,
- journeys => [@journeys],
+ $self->respond_to(
+ json => {
+ json => {
+ name => $name,
+ uid => $user->{id},
+ bio => $profile->{bio}{html},
+ metadata => $profile->{metadata},
+ }
+ },
+ any => {
+ template => 'profile',
+ title => "travelynx: $name",
+ name => $name,
+ uid => $user->{id},
+ privacy => $user,
+ bio => $profile->{bio}{html},
+ metadata => $profile->{metadata},
+ is_self => $is_self,
+ following => ( $relation and $relation eq 'follows' ) ? 1 : 0,
+ follow_requested => ( $relation and $relation eq 'requests_follow' )
+ ? 1
+ : 0,
+ can_follow =>
+ ( $my_user and $user->{accept_follows} and not $relation ) ? 1
+ : 0,
+ can_request_follow => (
+ $my_user and $user->{accept_follow_requests} and not $relation
+ ) ? 1
+ : 0,
+ follows_me =>
+ ( $inverse_relation and $inverse_relation eq 'follows' ) ? 1
+ : 0,
+ follow_reqs_me => (
+ $inverse_relation and $inverse_relation eq 'requests_follow'
+ ) ? 1
+ : 0,
+ journey => $status,
+ journeys => [@journeys],
+ with_map => 1,
+ %{$map_data},
+ }
);
}
@@ -220,12 +240,13 @@ sub journey_details {
}
my $journey = $self->journeys->get_single(
- uid => $user->{id},
- journey_id => $journey_id,
- verbose => 1,
- with_datetime => 1,
- with_polyline => 1,
- with_visibility => 1,
+ uid => $user->{id},
+ journey_id => $journey_id,
+ verbose => 1,
+ with_datetime => 1,
+ with_route_datetime => 1,
+ with_polyline => 1,
+ with_visibility => 1,
);
if ( not $journey ) {
@@ -334,7 +355,16 @@ sub user_status {
my $user = $self->users->get_privacy_by( name => $name );
if ( not $user ) {
- $self->render( 'not_found', status => 404 );
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404,
+ },
+ any => {
+ template => 'not_found',
+ status => 404
+ }
+ );
return;
}
@@ -389,11 +419,29 @@ sub user_status {
"/p/${name}/j/$journey->{id}?token=${token}-${ts}");
}
else {
- $self->render( 'not_found', status => 404 );
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404,
+ },
+ any => {
+ template => 'not_found',
+ status => 404
+ }
+ );
}
return;
}
- $self->render( 'not_found', status => 404 );
+ $self->respond_to(
+ json => {
+ json => { error => 'not found' },
+ status => 404,
+ },
+ any => {
+ template => 'not_found',
+ status => 404
+ }
+ );
return;
}
@@ -455,6 +503,13 @@ sub user_status {
$og_data{description} = $tw_data{description} = q{};
}
+ my $map_data = {};
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
+
$self->respond_to(
json => {
json => {
@@ -477,7 +532,9 @@ sub user_status {
journey => $status,
twitter => \%tw_data,
opengraph => \%og_data,
- version => $self->app->config->{version} // 'UNKNOWN',
+ with_map => 1,
+ %{$map_data},
+ version => $self->app->config->{version} // 'UNKNOWN',
},
);
}
@@ -516,6 +573,7 @@ sub status_card {
my $status = $self->get_user_status( $user->{id} );
my $visibility;
+ my $map_data = {};
if ( $status->{checked_in} or $status->{arr_name} ) {
my $visibility = $status->{effective_visibility};
if (
@@ -540,12 +598,19 @@ sub status_card {
$status->{arr_name} = undef;
}
+ if ( $status->{checked_in} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
+
$self->render(
'_public_status_card',
name => $name,
privacy => $user,
journey => $status,
from_profile => $self->param('profile') ? 1 : 0,
+ %{$map_data},
);
}
diff --git a/lib/Travelynx/Controller/Static.pm b/lib/Travelynx/Controller/Static.pm
index 04c2d0f..bcd6fda 100644
--- a/lib/Travelynx/Controller/Static.pm
+++ b/lib/Travelynx/Controller/Static.pm
@@ -35,4 +35,10 @@ sub offline {
$self->render('offline');
}
+sub tos {
+ my ($self) = @_;
+
+ $self->render('terms-of-service');
+}
+
1;
diff --git a/lib/Travelynx/Controller/Traewelling.pm b/lib/Travelynx/Controller/Traewelling.pm
index 3cdeff8..6aa789c 100644
--- a/lib/Travelynx/Controller/Traewelling.pm
+++ b/lib/Travelynx/Controller/Traewelling.pm
@@ -29,7 +29,7 @@ sub oauth {
redirect_uri =>
$self->base_url_for('/oauth/traewelling')->to_abs->scheme(
$self->app->mode eq 'development' ? 'http' : 'https'
- )->to_string,
+ )->to_string,
scope => 'read-statuses write-statuses'
}
)->then(
diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm
index 89385e1..fd2abb1 100755
--- a/lib/Travelynx/Controller/Traveling.pm
+++ b/lib/Travelynx/Controller/Traveling.pm
@@ -1,6 +1,7 @@
package Travelynx::Controller::Traveling;
# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
use Mojo::Base 'Mojolicious::Controller';
@@ -10,6 +11,7 @@ use DateTime::Format::Strptime;
use List::Util qw(uniq min max);
use List::UtilsBy qw(max_by uniq_by);
use List::MoreUtils qw(first_index);
+use Mojo::UserAgent;
use Mojo::Promise;
use Text::CSV;
use Travel::Status::DE::IRIS::Stations;
@@ -24,10 +26,15 @@ sub has_str_in_list {
return;
}
+# when called with "eva" provided: look up connections from eva, either
+# for provided backend_id / hafas or (if not provided) for user backend id.
+# When calld without "eva": look up connections from current/latest arrival
+# eva, using the checkin's backend id.
sub get_connecting_trains_p {
my ( $self, %opt ) = @_;
- my $uid = $opt{uid} //= $self->current_user->{id};
+ my $user = $self->current_user;
+ my $uid = $opt{uid} //= $user->{id};
my $use_history = $self->users->use_history( uid => $uid );
my ( $eva, $exclude_via, $exclude_train_id, $exclude_before );
@@ -36,6 +43,23 @@ sub get_connecting_trains_p {
my $promise = Mojo::Promise->new;
+ if ( $user->{backend_dbris} ) {
+
+ # We do get a little bit of via information, so this might work in some
+ # cases. But not reliably. Probably best to leave it out entirely then.
+ return $promise->reject;
+ }
+ if ( $user->{backend_efa} ) {
+
+ # TODO
+ return $promise->reject;
+ }
+ if ( $user->{backend_motis} ) {
+
+ # FIXME: The following code can't handle external_ids currently
+ return $promise->reject;
+ }
+
if ( $opt{eva} ) {
if ( $use_history & 0x01 ) {
$eva = $opt{eva};
@@ -43,10 +67,20 @@ sub get_connecting_trains_p {
elsif ( $opt{destination_name} ) {
$eva = $opt{eva};
}
+ if ( not defined $opt{backend_id} ) {
+ if ( $opt{hafas} ) {
+ $opt{backend_id}
+ = $self->stations->get_backend_id( hafas => $opt{hafas} );
+ }
+ else {
+ $opt{backend_id} = $user->{backend_id};
+ }
+ }
}
else {
if ( $use_history & 0x02 ) {
my $status = $self->get_user_status;
+ $opt{backend_id} = $status->{backend_id};
$eva = $status->{arr_eva};
$exclude_via = $status->{dep_name};
$exclude_train_id = $status->{train_id};
@@ -65,10 +99,12 @@ sub get_connecting_trains_p {
return $promise->reject;
}
- my ( $dest_ids, $destinations )
- = $self->journeys->get_connection_targets(%opt);
+ $self->log->debug(
+ "get_connecting_trains_p(backend_id => $opt{backend_id}, eva => $eva)");
- my @destinations = uniq_by { $_->{name} } @{$destinations};
+ my @destinations = $self->journeys->get_connection_targets(%opt);
+
+ @destinations = uniq_by { $_->{name} } @destinations;
if ($exclude_via) {
@destinations = grep { $_->{name} ne $exclude_via } @destinations;
@@ -78,11 +114,8 @@ sub get_connecting_trains_p {
return $promise->reject;
}
- my $iris_eva = $eva;
- if ( $eva < 8000000 ) {
- $iris_eva = ( List::Util::first { $_ >= 8000000 } @{$dest_ids} )
- // $eva;
- }
+ $self->log->debug( 'get_connection_targets returned '
+ . join( q{, }, map { $_->{name} } @destinations ) );
my $can_check_in = not $arr_epoch || ( $arr_countdown // 1 ) < 0;
my $lookahead
@@ -91,11 +124,11 @@ sub get_connecting_trains_p {
my $iris_promise = Mojo::Promise->new;
my %via_count = map { $_->{name} => 0 } @destinations;
- if ( $iris_eva >= 8000000
- and List::Util::any { $_->{eva} >= 8000000 } @destinations )
- {
+ my $backend
+ = $self->stations->get_backend( backend_id => $opt{backend_id} );
+ if ( $opt{backend_id} == 0 ) {
$self->iris->get_departures_p(
- station => $iris_eva,
+ station => $eva,
lookbehind => 10,
lookahead => $lookahead,
with_related => 1
@@ -103,7 +136,7 @@ sub get_connecting_trains_p {
sub {
my ($stationboard) = @_;
if ( $stationboard->{errstr} ) {
- $iris_promise->resolve( [] );
+ $promise->resolve( [], [] );
return;
}
@@ -237,105 +270,35 @@ sub get_connecting_trains_p {
}
}
- $iris_promise->resolve( [ @results, @cancellations ] );
+ $promise->resolve( [ @results, @cancellations ], [] );
return;
}
)->catch(
sub {
- $iris_promise->resolve( [] );
+ $promise->resolve( [], [] );
return;
}
)->wait;
}
- else {
- $iris_promise->resolve( [] );
+ elsif ( $backend->{dbris} ) {
+ return $promise->reject;
}
-
- my $hafas_promise = Mojo::Promise->new;
- $self->hafas->get_departures_p(
- eva => $eva,
- lookbehind => 10,
- lookahead => $lookahead
- )->then(
- sub {
- my ($status) = @_;
- $hafas_promise->resolve( [ $status->results ] );
- return;
- }
- )->catch(
- sub {
- # HAFAS data is optional.
- # Errors are logged by get_json_p and can be silently ignored here.
- $hafas_promise->resolve( [] );
- return;
- }
- )->wait;
-
- Mojo::Promise->all( $iris_promise, $hafas_promise )->then(
- sub {
- my ( $iris, $hafas ) = @_;
- my @iris_trains = @{ $iris->[0] };
- my @all_hafas_trains = @{ $hafas->[0] };
- my @hafas_trains;
-
- # We've already got a list of connecting trains; this function
- # only adds further information to them. We ignore errors, as
- # partial data is better than no data.
- eval {
- for my $iris_train (@iris_trains) {
- if ( $iris_train->[0]->departure_is_cancelled ) {
- for my $hafas_train (@all_hafas_trains) {
- if ( $hafas_train->number
- and $hafas_train->number
- == $iris_train->[0]->train_no )
- {
- $hafas_train->{iris_seen} = 1;
- next;
- }
- }
- next;
- }
- for my $hafas_train (@all_hafas_trains) {
- if ( $hafas_train->number
- and $hafas_train->number
- == $iris_train->[0]->train_no )
- {
- $hafas_train->{iris_seen} = 1;
- if ( $hafas_train->load
- and $hafas_train->load->{SECOND} )
- {
- $iris_train->[3] = $hafas_train->load;
- }
- for my $stop ( $hafas_train->route ) {
- if ( $stop->loc->name
- and $stop->loc->name eq
- $iris_train->[1]->{name}
- and $stop->arr )
- {
- $iris_train->[2] = $stop->arr;
- if ( $iris_train->[0]->departure_delay
- and not $stop->arr_delay )
- {
- $iris_train->[2]
- ->add( minutes => $iris_train->[0]
- ->departure_delay );
- }
- last;
- }
- }
- last;
- }
- }
- }
+ elsif ( $backend->{efa} ) {
+ return $promise->reject;
+ }
+ elsif ( $backend->{hafas} ) {
+ my $hafas_service = $backend->{name};
+ $self->hafas->get_departures_p(
+ service => $hafas_service,
+ eva => $eva,
+ lookbehind => 10,
+ lookahead => $lookahead
+ )->then(
+ sub {
+ my ($status) = @_;
+ my @hafas_trains;
+ my @all_hafas_trains = $status->results;
for my $hafas_train (@all_hafas_trains) {
- if ( $hafas_train->{iris_seen} ) {
- next;
- }
- if ( $hafas_train->station_eva >= 8000000 ) {
-
- # better safe than sorry, for now
- next;
- }
for my $stop ( $hafas_train->route ) {
for my $dest (@destinations) {
if ( $stop->loc->name
@@ -353,30 +316,30 @@ sub get_connecting_trains_p {
}
if ( $departure->epoch >= $exclude_before ) {
$via_count{ $dest->{name} }++;
- push( @hafas_trains,
- [ $hafas_train, $dest, $arrival ] );
+ push(
+ @hafas_trains,
+ [
+ $hafas_train, $dest,
+ $arrival, $hafas_service
+ ]
+ );
}
}
}
}
}
- };
- if ($@) {
- $self->app->log->error(
- "get_connecting_trains_p($uid): IRIS/HAFAS merge failed: $@"
- );
+ $promise->resolve( [], \@hafas_trains );
+ return;
}
-
- $promise->resolve( \@iris_trains, \@hafas_trains );
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $promise->reject($err);
- return;
- }
- )->wait;
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->log->debug("get_connection_trains: hafas: $err");
+ $promise->resolve( [], [] );
+ return;
+ }
+ )->wait;
+ }
return $promise;
}
@@ -394,7 +357,8 @@ sub compute_effective_visibility {
sub homepage {
my ($self) = @_;
if ( $self->is_user_authenticated ) {
- my $uid = $self->current_user->{id};
+ my $user = $self->current_user;
+ my $uid = $user->{id};
my $status = $self->get_user_status;
my @timeline = $self->in_transit->get_timeline(
uid => $uid,
@@ -403,9 +367,15 @@ sub homepage {
$self->stash( timeline => [@timeline] );
my @recent_targets;
if ( $status->{checked_in} ) {
+ my $map_data = {};
+ if ( $status->{arr_name} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
my $journey_visibility
= $self->compute_effective_visibility(
- $self->current_user->{default_visibility_str},
+ $user->{default_visibility_str},
$status->{visibility_str} );
if ( defined $status->{arrival_countdown}
and $status->{arrival_countdown} < ( 40 * 60 ) )
@@ -416,10 +386,13 @@ sub homepage {
my ( $connections_iris, $connections_hafas ) = @_;
$self->render(
'landingpage',
+ user => $user,
user_status => $status,
journey_visibility => $journey_visibility,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
}
@@ -427,8 +400,11 @@ sub homepage {
sub {
$self->render(
'landingpage',
+ user => $user,
user_status => $status,
journey_visibility => $journey_visibility,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
}
@@ -438,23 +414,28 @@ sub homepage {
else {
$self->render(
'landingpage',
+ user => $user,
user_status => $status,
journey_visibility => $journey_visibility,
+ with_map => 1,
+ %{$map_data},
);
$self->users->mark_seen( uid => $uid );
return;
}
}
else {
- @recent_targets = uniq_by { $_->{eva} }
+ @recent_targets = uniq_by { $_->{external_id_or_eva} }
$self->journeys->get_latest_checkout_stations( uid => $uid );
}
$self->render(
'landingpage',
+ user => $user,
user_status => $status,
recent_targets => \@recent_targets,
with_autocomplete => 1,
- with_geolocation => 1
+ with_geolocation => 1,
+ backend_id => $user->{backend_id},
);
$self->users->mark_seen( uid => $uid );
}
@@ -476,6 +457,12 @@ sub status_card {
$self->stash( timeline => [@timeline] );
if ( $status->{checked_in} ) {
+ my $map_data = {};
+ if ( $status->{arr_name} ) {
+ $map_data = $self->journeys_to_map_data(
+ journeys => [$status],
+ );
+ }
my $journey_visibility
= $self->compute_effective_visibility(
$self->current_user->{default_visibility_str},
@@ -493,6 +480,7 @@ sub status_card {
journey_visibility => $journey_visibility,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
+ %{$map_data},
);
}
)->catch(
@@ -501,6 +489,7 @@ sub status_card {
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
+ %{$map_data},
);
}
)->wait;
@@ -510,11 +499,13 @@ sub status_card {
'_checked_in',
journey => $status,
journey_visibility => $journey_visibility,
+ %{$map_data},
);
}
elsif ( $status->{cancellation} ) {
$self->render_later;
$self->get_connecting_trains_p(
+ backend_id => $status->{backend_id},
eva => $status->{cancellation}{dep_eva},
destination_name => $status->{cancellation}{arr_name}
)->then(
@@ -563,14 +554,225 @@ sub status_card {
sub geolocation {
my ($self) = @_;
- my $lon = $self->param('lon');
- my $lat = $self->param('lat');
+ my $lon = $self->param('lon');
+ my $lat = $self->param('lat');
+ my $backend_id = $self->param('backend') // 0;
if ( not $lon or not $lat ) {
- $self->render( json => { error => 'Invalid lon/lat received' } );
+ $self->render(
+ json => { error => "Invalid lon/lat (${lon}/${lat}) received" } );
+ return;
+ }
+
+ if ( $backend_id !~ m{ ^ \d+ $ }x ) {
+ $self->render(
+ json => { error => "Invalid backend (${backend_id}) received" } );
+ return;
+ }
+
+ my ( $dbris_service, $efa_service, $hafas_service, $motis_service );
+ my $backend = $self->stations->get_backend( backend_id => $backend_id );
+ if ( $backend->{dbris} ) {
+ $dbris_service = $backend->{name};
+ }
+ if ( $backend->{efa} ) {
+ $efa_service = $backend->{name};
+ }
+ elsif ( $backend->{hafas} ) {
+ $hafas_service = $backend->{name};
+ }
+ elsif ( $backend->{motis} ) {
+ $motis_service = $backend->{name};
+ }
+
+ if ($dbris_service) {
+ $self->render_later;
+
+ Travel::Status::DE::DBRIS->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ geoSearch => {
+ latitude => $lat,
+ longitude => $lon
+ }
+ )->then(
+ sub {
+ my ($dbris) = @_;
+ my @results = map {
+ {
+ name => $_->name,
+ eva => $_->eva,
+ distance => 0,
+ dbris => $dbris_service,
+ }
+ } $dbris->results;
+ if ( @results > 10 ) {
+ @results = @results[ 0 .. 9 ];
+ }
+ $self->render(
+ json => {
+ candidates => [@results],
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ warning => $err,
+ }
+ );
+ }
+ )->wait;
+ return;
+ }
+ elsif ($efa_service) {
+ $self->render_later;
+
+ Travel::Status::DE::EFA->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ service => $efa_service,
+ coord => {
+ lat => $lat,
+ lon => $lon
+ }
+ )->then(
+ sub {
+ my ($efa) = @_;
+ my @results = map {
+ {
+ name => $_->full_name,
+ eva => $_->id_code,
+ distance => 0,
+ efa => $efa_service,
+ }
+ } $efa->results;
+ if ( @results > 10 ) {
+ @results = @results[ 0 .. 9 ];
+ }
+ $self->render(
+ json => {
+ candidates => [@results],
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ warning => $err,
+ }
+ );
+ }
+ )->wait;
+ return;
+ }
+ elsif ($hafas_service) {
+ $self->render_later;
+
+ my $agent = $self->ua;
+ if ( my $proxy = $self->app->config->{hafas}{$hafas_service}{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
+ Travel::Status::DE::HAFAS->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => $agent,
+ service => $hafas_service,
+ geoSearch => {
+ lat => $lat,
+ lon => $lon
+ }
+ )->then(
+ sub {
+ my ($hafas) = @_;
+ my @hafas = map {
+ {
+ name => $_->name,
+ eva => $_->eva,
+ distance => $_->distance_m / 1000,
+ hafas => $hafas_service
+ }
+ } $hafas->results;
+ if ( @hafas > 10 ) {
+ @hafas = @hafas[ 0 .. 9 ];
+ }
+ $self->render(
+ json => {
+ candidates => [@hafas],
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ warning => $err,
+ }
+ );
+ }
+ )->wait;
+
+ return;
+ }
+ elsif ($motis_service) {
+ $self->render_later;
+
+ Travel::Status::MOTIS->new_p(
+ promise => 'Mojo::Promise',
+ user_agent => $self->ua,
+ time_zone => 'Europe/Berlin',
+
+ service => $motis_service,
+ stops_by_coordinate => {
+ lat => $lat,
+ lon => $lon
+ }
+ )->then(
+ sub {
+ my ($motis) = @_;
+ my @motis = map {
+ {
+ id => $_->id,
+ name => $_->name,
+ distance => 0,
+ motis => $motis_service,
+ }
+ } $motis->results;
+
+ if ( @motis > 10 ) {
+ @motis = @motis[ 0 .. 9 ];
+ }
+
+ $self->render(
+ json => {
+ candidates => [@motis],
+ }
+ );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->render(
+ json => {
+ candidates => [],
+ warning => $err,
+ }
+ );
+ }
+ )->wait;
+
return;
}
- $self->render_later;
my @iris = map {
{
@@ -588,48 +790,12 @@ sub geolocation {
if ( @iris > 5 ) {
@iris = @iris[ 0 .. 4 ];
}
-
- Travel::Status::DE::HAFAS->new_p(
- promise => 'Mojo::Promise',
- user_agent => $self->ua,
- geoSearch => {
- lat => $lat,
- lon => $lon
- }
- )->then(
- sub {
- my ($hafas) = @_;
- my @hafas = map {
- {
- name => $_->name,
- eva => $_->eva,
- distance => $_->distance_m / 1000,
- hafas => 1
- }
- } $hafas->results;
- if ( @hafas > 10 ) {
- @hafas = @hafas[ 0 .. 9 ];
- }
- my @results = map { $_->[0] }
- sort { $a->[1] <=> $b->[1] }
- map { [ $_, $_->{distance} ] } ( @iris, @hafas );
- $self->render(
- json => {
- candidates => [@results],
- }
- );
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->render(
- json => {
- candidates => [@iris],
- warning => $err,
- }
- );
+ $self->render(
+ json => {
+ candidates => [@iris],
}
- )->wait;
+ );
+
}
sub travel_action {
@@ -684,8 +850,14 @@ sub travel_action {
$promise->then(
sub {
return $self->checkin_p(
- station => $params->{station},
- train_id => $params->{train}
+ dbris => $params->{dbris},
+ efa => $params->{efa},
+ hafas => $params->{hafas},
+ motis => $params->{motis},
+ station => $params->{station},
+ train_id => $params->{train},
+ train_suffix => $params->{suffix},
+ ts => $params->{ts},
);
}
)->then(
@@ -713,8 +885,14 @@ sub travel_action {
my ( $still_checked_in, undef ) = @_;
if ( my $destination = $params->{dest} ) {
my $station_link = '/s/' . $destination;
- if ( $status->{train_id} =~ m{[|]} ) {
- $station_link .= '?hafas=1';
+ if ( $status->{is_dbris} ) {
+ $station_link .= '?dbris=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_efa} ) {
+ $station_link .= '?efa=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_hafas} ) {
+ $station_link .= '?hafas=' . $status->{backend_name};
}
$self->render(
json => {
@@ -749,8 +927,14 @@ sub travel_action {
sub {
my ( $still_checked_in, $error ) = @_;
my $station_link = '/s/' . $params->{station};
- if ( $status->{train_id} =~ m{[|]} ) {
- $station_link .= '?hafas=1';
+ if ( $status->{is_dbris} ) {
+ $station_link .= '?dbris=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_efa} ) {
+ $station_link .= '?efa=' . $status->{backend_name};
+ }
+ elsif ( $status->{is_hafas} ) {
+ $station_link .= '?hafas=' . $status->{backend_name};
}
if ($error) {
@@ -800,8 +984,32 @@ sub travel_action {
else {
my $redir = '/';
if ( $status->{checked_in} or $status->{cancelled} ) {
- if ( $status->{train_id} =~ m{[|]} ) {
- $redir = '/s/' . $status->{dep_eva} . '?hafas=1';
+ if ( $status->{is_dbris} ) {
+ $redir
+ = '/s/'
+ . $status->{dep_eva}
+ . '?dbris='
+ . $status->{backend_name};
+ }
+ elsif ( $status->{is_efa} ) {
+ $redir
+ = '/s/'
+ . $status->{dep_eva} . '?efa='
+ . $status->{backend_name};
+ }
+ elsif ( $status->{is_hafas} ) {
+ $redir
+ = '/s/'
+ . $status->{dep_eva}
+ . '?hafas='
+ . $status->{backend_name};
+ }
+ elsif ( $status->{is_motis} ) {
+ $redir
+ = '/s/'
+ . $status->{dep_external_id}
+ . '?motis='
+ . $status->{backend_name};
}
else {
$redir = '/s/' . $status->{dep_ds100};
@@ -818,8 +1026,13 @@ sub travel_action {
elsif ( $params->{action} eq 'cancelled_from' ) {
$self->render_later;
$self->checkin_p(
+ dbris => $params->{dbris},
+ efa => $params->{efa},
+ hafas => $params->{hafas},
+ motis => $params->{motis},
station => $params->{station},
- train_id => $params->{train}
+ train_id => $params->{train},
+ ts => $params->{ts},
)->then(
sub {
$self->render(
@@ -920,7 +1133,8 @@ sub station {
my $train = $self->param('train');
my $trip_id = $self->param('trip_id');
my $timestamp = $self->param('timestamp');
- my $uid = $self->current_user->{id};
+ my $user = $self->current_user;
+ my $uid = $user->{id};
my @timeline = $self->in_transit->get_timeline(
uid => $uid,
@@ -928,7 +1142,6 @@ sub station {
);
my %checkin_by_train;
for my $checkin (@timeline) {
- say $checkin->{train_id};
push( @{ $checkin_by_train{ $checkin->{train_id} } }, $checkin );
}
$self->stash( checkin_by_train => \%checkin_by_train );
@@ -945,16 +1158,105 @@ sub station {
$timestamp = DateTime->now( time_zone => 'Europe/Berlin' );
}
- my $use_hafas = $self->param('hafas');
+ my ( $dbris_service, $efa_service, $hafas_service, $motis_service );
+
+ if ( $self->param('dbris') ) {
+ $dbris_service = $self->param('dbris');
+ }
+ elsif ( $self->param('efa') ) {
+ $efa_service = $self->param('efa');
+ }
+ elsif ( $self->param('hafas') ) {
+ $hafas_service = $self->param('hafas');
+ }
+ elsif ( $self->param('motis') ) {
+ $motis_service = $self->param('motis');
+ }
+ else {
+ if ( $user->{backend_dbris} ) {
+ $dbris_service = $user->{backend_name};
+ }
+ elsif ( $user->{backend_efa} ) {
+ $efa_service = $user->{backend_name};
+ }
+ elsif ( $user->{backend_hafas} ) {
+ $hafas_service = $user->{backend_name};
+ }
+ elsif ( $user->{backend_motis} ) {
+ $motis_service = $user->{backend_name};
+ }
+ }
+
my $promise;
- if ($use_hafas) {
+ if ($dbris_service) {
+ if ( $station !~ m{ [@] L = \d+ }x ) {
+ $self->render_later;
+ $self->dbris->get_station_id_p($station)->then(
+ sub {
+ my ($dbris_station) = @_;
+ $self->redirect_to( '/s/' . $dbris_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->redirect_to('/');
+ }
+ )->wait;
+ return;
+ }
+ $promise = $self->dbris->get_departures_p(
+ station => $station,
+ timestamp => $timestamp,
+ lookbehind => 30,
+ );
+ }
+ elsif ($efa_service) {
+ $promise = $self->efa->get_departures_p(
+ service => $efa_service,
+ name => $station,
+ timestamp => $timestamp,
+ lookbehind => 10,
+ lookahead => 50,
+ );
+ }
+ elsif ($hafas_service) {
$promise = $self->hafas->get_departures_p(
+ service => $hafas_service,
eva => $station,
timestamp => $timestamp,
lookbehind => 30,
lookahead => 30,
);
}
+ elsif ($motis_service) {
+ if ( $station !~ m/.*_.*/ ) {
+ $self->render_later;
+ $self->motis->get_station_by_query_p(
+ service => $motis_service,
+ query => $station,
+ )->then(
+ sub {
+ my ($motis_station) = @_;
+ $self->redirect_to( '/s/' . $motis_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ say "$err";
+
+ $self->redirect_to('/');
+ }
+ )->wait;
+ return;
+ }
+ $promise = $self->motis->get_departures_p(
+ service => $motis_service,
+ station_id => $station,
+ timestamp => $timestamp,
+ lookbehind => 30,
+ lookahead => 30,
+ );
+ }
else {
$promise = $self->iris->get_departures_p(
station => $station,
@@ -966,28 +1268,39 @@ sub station {
$promise->then(
sub {
my ($status) = @_;
- my $api_link;
my @results;
my $now = $self->now->epoch;
my $now_within_range
= abs( $timestamp->epoch - $now ) < 1800 ? 1 : 0;
- if ($use_hafas) {
+ if ($dbris_service) {
+
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->dep->epoch ] } $status->results;
+
+ $status = {
+ station_eva => $station,
+ related_stations => [],
+ };
- my $iris_eva = List::Util::min grep { $_ >= 1000000 }
- @{ $status->station->{evas} // [] };
- if ($iris_eva) {
- $api_link = '/s/' . $iris_eva;
+ if ( $station =~ m{ [@] O = (?<name> [^@]+ ) [@] }x ) {
+ $status->{station_name} = $+{name};
}
+ }
+ elsif ($hafas_service) {
@results = map { $_->[0] }
sort { $b->[1] <=> $a->[1] }
map { [ $_, $_->datetime->epoch ] } $status->results;
- $self->stations->add_meta(
- eva => $status->station->{eva},
- meta => $status->station->{evas} // []
- );
+ if ( $status->station->{eva} ) {
+ $self->stations->add_meta(
+ eva => $status->station->{eva},
+ meta => $status->station->{evas} // [],
+ hafas => $hafas_service,
+ );
+ }
$status = {
station_eva => $status->station->{eva},
station_name => (
@@ -997,9 +1310,30 @@ sub station {
related_stations => [],
};
}
- else {
+ elsif ($efa_service) {
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->datetime->epoch ] } $status->results;
+ $status = {
+ station_eva => $status->stop->id_num,
+ station_name => $status->stop->full_name,
+ related_stations => [],
+ };
+ }
+ elsif ($motis_service) {
+ @results = map { $_->[0] }
+ sort { $b->[1] <=> $a->[1] }
+ map { [ $_, $_->stopover->departure->epoch ] }
+ $status->results;
- $api_link = '/s/' . $status->{station_eva} . '?hafas=1';
+ $status = {
+ station_eva => $station,
+ station_name =>
+ $status->{results}->[0]->stopover->stop->name,
+ related_stations => [],
+ };
+ }
+ else {
# You can't check into a train which terminates here
@results = grep { $_->departure } @{ $status->{results} };
@@ -1029,10 +1363,10 @@ sub station {
}
my $connections_p;
- if ( $trip_id and $use_hafas ) {
+ if ( $trip_id and ( $dbris_service or $hafas_service ) ) {
@results = grep { $_->id eq $trip_id } @results;
}
- elsif ( $train and not $use_hafas ) {
+ elsif ( $train and not $hafas_service ) {
@results
= grep { $_->type . ' ' . $_->train_no eq $train } @results;
}
@@ -1044,12 +1378,17 @@ sub station {
$connections_p = $self->get_connecting_trains_p(
eva => $user_status->{cancellation}{dep_eva},
destination_name =>
- $user_status->{cancellation}{arr_name}
+ $user_status->{cancellation}{arr_name},
+ efa => $efa_service,
+ hafas => $hafas_service,
);
}
else {
$connections_p = $self->get_connecting_trains_p(
- eva => $status->{station_eva} );
+ eva => $status->{station_eva},
+ efa => $efa_service,
+ hafas => $hafas_service
+ );
}
}
@@ -1059,18 +1398,21 @@ sub station {
my ( $connections_iris, $connections_hafas ) = @_;
$self->render(
'departures',
+ user => $user,
+ dbris => $dbris_service,
+ efa => $efa_service,
+ hafas => $hafas_service,
+ motis => $motis_service,
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
results => \@results,
- hafas => $use_hafas,
station => $status->{station_name},
related_stations => $status->{related_stations},
user_status => $user_status,
can_check_out => $can_check_out,
connections_iris => $connections_iris,
connections_hafas => $connections_hafas,
- api_link => $api_link,
title => "travelynx: $status->{station_name}",
);
}
@@ -1078,16 +1420,19 @@ sub station {
sub {
$self->render(
'departures',
+ user => $user,
+ dbris => $dbris_service,
+ efa => $efa_service,
+ hafas => $hafas_service,
+ motis => $motis_service,
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
results => \@results,
- hafas => $use_hafas,
station => $status->{station_name},
related_stations => $status->{related_stations},
user_status => $user_status,
can_check_out => $can_check_out,
- api_link => $api_link,
title => "travelynx: $status->{station_name}",
);
}
@@ -1096,16 +1441,19 @@ sub station {
else {
$self->render(
'departures',
+ user => $user,
+ dbris => $dbris_service,
+ efa => $efa_service,
+ hafas => $hafas_service,
+ motis => $motis_service,
eva => $status->{station_eva},
datetime => $timestamp,
now_in_range => $now_within_range,
results => \@results,
- hafas => $use_hafas,
station => $status->{station_name},
related_stations => $status->{related_stations},
user_status => $user_status,
can_check_out => $can_check_out,
- api_link => $api_link,
title => "travelynx: $status->{station_name}",
);
}
@@ -1120,15 +1468,35 @@ sub station {
status => 300,
);
}
- elsif ( $use_hafas and $status and $status->errcode eq 'LOCATION' )
+ elsif ( $efa_service
+ and $status
+ and scalar $status->name_candidates )
+ {
+ $self->render(
+ 'disambiguation',
+ suggestions => [
+ map { { name => $_->name, eva => $_->id_num } }
+ $status->name_candidates
+ ],
+ status => 300,
+ );
+ }
+ elsif ( $hafas_service
+ and $status
+ and $status->errcode eq 'LOCATION' )
{
- $self->hafas->search_location_p( query => $station )->then(
+ $self->hafas->search_location_p(
+ service => $hafas_service,
+ query => $station
+ )->then(
sub {
my ($hafas2) = @_;
my @suggestions = $hafas2->results;
if ( @suggestions == 1 ) {
- $self->redirect_to(
- '/s/' . $suggestions[0]->eva . '?hafas=1' );
+ $self->redirect_to( '/s/'
+ . $suggestions[0]->eva
+ . '?hafas='
+ . $hafas_service );
}
else {
$self->render(
@@ -1153,11 +1521,30 @@ sub station {
}
)->wait;
}
+ elsif ( $err
+ =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden|HTTP 500 Internal Server Error}
+ )
+ {
+ $self->render(
+ 'bad_gateway',
+ message => $err,
+ status => 502,
+ select_new_backend => 1,
+ );
+ }
+ elsif ( $err =~ m{timeout}i ) {
+ $self->render(
+ 'gateway_timeout',
+ message => $err,
+ status => 504,
+ select_new_backend => 1,
+ );
+ }
else {
$self->render(
'exception',
exception => $err,
- status => 502
+ status => 500
);
}
}
@@ -1169,25 +1556,49 @@ sub redirect_to_station {
my ($self) = @_;
my $station = $self->param('station');
- if ( my $s = $self->app->stations->search($station) ) {
- if ( $s->{source} == 1 ) {
- $self->redirect_to("/s/${station}?hafas=1");
- }
- else {
- $self->redirect_to("/s/${station}");
- }
+ if ( $self->param('backend_dbris') ) {
+ $self->render_later;
+ $self->dbris->get_station_id_p($station)->then(
+ sub {
+ my ($dbris_station) = @_;
+ $self->redirect_to( '/s/' . $dbris_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->redirect_to('/');
+ }
+ )->wait;
+ }
+ elsif ( $self->param('backend_motis') ) {
+ $self->render_later;
+ $self->motis->get_station_by_query(
+ service => $self->param('backend_motis'),
+ query => $station,
+ )->then(
+ sub {
+ my ($motis_station) = @_;
+ $self->redirect_to( '/s/' . $motis_station->{id} );
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->redirect_to('/');
+ }
+ )->wait;
}
else {
- $self->redirect_to("/s/${station}?hafas=1");
+ $self->redirect_to("/s/${station}");
}
}
sub cancelled {
my ($self) = @_;
my @journeys = $self->journeys->get(
- uid => $self->current_user->{id},
- cancelled => 1,
- with_datetime => 1
+ uid => $self->current_user->{id},
+ cancelled => 1,
+ with_datetime => 1,
+ with_route_datetime => 1
);
$self->respond_to(
@@ -1323,8 +1734,6 @@ sub commute {
sub map_history {
my ($self) = @_;
- my $location = $self->app->coordinates_by_station;
-
if ( not $self->param('route_type') ) {
$self->param( route_type => 'polybee' );
}
@@ -1430,15 +1839,19 @@ sub csv_history {
my $buf = q{};
$csv->combine(
- qw(Zugtyp Linie Nummer Start Ziel),
- 'Start (DS100)',
- 'Ziel (DS100)',
- 'Abfahrt (soll)',
- 'Abfahrt (ist)',
- 'Ankunft (soll)',
- 'Ankunft (ist)',
- 'Kommentar',
- 'ID'
+ qw(type line number),
+ 'departure stop name',
+ 'departure stop id',
+ 'arrival stop name',
+ 'arrival stop id',
+ 'scheduled departure',
+ 'real-time departure',
+ 'scheduled arrival',
+ 'real-time arrival',
+ 'operator',
+ 'carriage type',
+ 'comment',
+ 'id'
);
$buf .= $csv->string;
@@ -1455,13 +1868,17 @@ sub csv_history {
$journey->{line},
$journey->{no},
$journey->{from_name},
+ $journey->{from_eva},
$journey->{to_name},
- $journey->{from_ds100},
- $journey->{to_ds100},
- $journey->{sched_departure}->strftime('%Y-%m-%d %H:%M'),
- $journey->{rt_departure}->strftime('%Y-%m-%d %H:%M'),
- $journey->{sched_arrival}->strftime('%Y-%m-%d %H:%M'),
- $journey->{rt_arrival}->strftime('%Y-%m-%d %H:%M'),
+ $journey->{to_eva},
+ $journey->{sched_departure}->strftime('%Y-%m-%d %H:%M:%S'),
+ $journey->{rt_departure}->strftime('%Y-%m-%d %H:%M:%S'),
+ $journey->{sched_arrival}->strftime('%Y-%m-%d %H:%M:%S'),
+ $journey->{rt_arrival}->strftime('%Y-%m-%d %H:%M:%S'),
+ $journey->{user_data}{operator} // q{},
+ join( q{ + },
+ map { $_->{desc} // $_->{name} }
+ @{ $journey->{user_data}{wagongroups} // [] } ),
$journey->{user_data}{comment} // q{},
$journey->{id}
)
@@ -1510,7 +1927,7 @@ sub year_in_review {
if ( not @journeys ) {
$self->render(
'not_found',
- message => 'Keine Zugfahrten im angefragten Jahr gefunden.',
+ message => 'Keine Fahrten im angefragten Jahr gefunden.',
status => 404
);
return;
@@ -1583,7 +2000,7 @@ sub yearly_history {
$self->render(
'not_found',
status => 404,
- message => 'Keine Zugfahrten im angefragten Jahr gefunden.'
+ message => 'Keine Fahrten im angefragten Jahr gefunden.'
);
return;
}
@@ -1660,7 +2077,7 @@ sub monthly_history {
if ( not @journeys ) {
$self->render(
'not_found',
- message => 'Keine Zugfahrten im angefragten Monat gefunden.',
+ message => 'Keine Fahrten im angefragten Monat gefunden.',
status => 404
);
return;
@@ -1682,13 +2099,15 @@ sub monthly_history {
}
},
any => {
- template => 'history_by_month',
- title => "travelynx: $month_name $year",
- journeys => [@journeys],
- year => $year,
- month => $month,
- month_name => $month_name,
- statistics => $stats
+ template => 'history_by_month',
+ title => "travelynx: $month_name $year",
+ journeys => [@journeys],
+ year => $year,
+ month => $month,
+ month_name => $month_name,
+ filter_from => $interval_start,
+ filter_to => $interval_end->clone->subtract( days => 1 ),
+ statistics => $stats
}
);
@@ -1714,12 +2133,13 @@ sub journey_details {
}
my $journey = $self->journeys->get_single(
- uid => $uid,
- journey_id => $journey_id,
- verbose => 1,
- with_datetime => 1,
- with_polyline => 1,
- with_visibility => 1,
+ uid => $uid,
+ journey_id => $journey_id,
+ verbose => 1,
+ with_datetime => 1,
+ with_route_datetime => 1,
+ with_polyline => 1,
+ with_visibility => 1,
);
if ($journey) {
@@ -1942,10 +2362,11 @@ sub edit_journey {
}
my $journey = $self->journeys->get_single(
- uid => $uid,
- journey_id => $journey_id,
- verbose => 1,
- with_datetime => 1,
+ uid => $uid,
+ journey_id => $journey_id,
+ verbose => 1,
+ with_datetime => 1,
+ with_route_datetime => 1,
);
if ( not $journey ) {
@@ -2046,11 +2467,12 @@ sub edit_journey {
if ( not $error ) {
$journey = $self->journeys->get_single(
- uid => $uid,
- db => $db,
- journey_id => $journey_id,
- verbose => 1,
- with_datetime => 1,
+ uid => $uid,
+ db => $db,
+ journey_id => $journey_id,
+ verbose => 1,
+ with_datetime => 1,
+ with_route_datetime => 1,
);
$error = $self->journeys->sanity_check($journey);
}
@@ -2092,6 +2514,8 @@ sub edit_journey {
sub add_journey_form {
my ($self) = @_;
+ $self->stash( backend_id => $self->current_user->{backend_id} );
+
if ( $self->param('action') and $self->param('action') eq 'save' ) {
my $parser = DateTime::Format::Strptime->new(
pattern => '%d.%m.%Y %H:%M',
@@ -2112,8 +2536,9 @@ sub add_journey_form {
$self->render(
'add_journey',
with_autocomplete => 1,
+ status => 400,
error =>
-'Zug muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
+'Fahrt muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
);
return;
}
@@ -2126,6 +2551,7 @@ sub add_journey_form {
$self->render(
'add_journey',
with_autocomplete => 1,
+ status => 400,
error => "${key}: Ungültiges Datums-/Zeitformat"
);
return;
@@ -2148,8 +2574,9 @@ sub add_journey_form {
my $db = $self->pg->db;
my $tx = $db->begin;
- $opt{db} = $db;
- $opt{uid} = $self->current_user->{id};
+ $opt{db} = $db;
+ $opt{uid} = $self->current_user->{id};
+ $opt{backend_id} = $self->current_user->{backend_id};
my ( $journey_id, $error ) = $self->journeys->add(%opt);
@@ -2167,6 +2594,7 @@ sub add_journey_form {
$self->render(
'add_journey',
with_autocomplete => 1,
+ status => 400,
error => $error,
);
}
@@ -2184,4 +2612,241 @@ sub add_journey_form {
}
}
+sub add_intransit_form {
+ my ($self) = @_;
+
+ $self->stash( backend_id => $self->current_user->{backend_id} );
+
+ if ( $self->param('action') and $self->param('action') eq 'save' ) {
+ my $parser = DateTime::Format::Strptime->new(
+ pattern => '%d.%m.%Y %H:%M',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
+ my %opt;
+ my %trip;
+
+ my @parts = split( qr{\s+}, $self->param('train') );
+
+ if ( @parts == 2 ) {
+ @trip{ 'train_type', 'train_no' } = @parts;
+ }
+ elsif ( @parts == 3 ) {
+ @trip{ 'train_type', 'train_line', 'train_no' } = @parts;
+ }
+ else {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error =>
+'Fahrt muss als „Typ Nummer“ oder „Typ Linie Nummer“ eingegeben werden.'
+ );
+ return;
+ }
+
+ for my $key (qw(sched_departure sched_arrival)) {
+ if ( $self->param($key) ) {
+ my $datetime = $parser->parse_datetime( $self->param($key) );
+ if ( not $datetime ) {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => "${key}: Ungültiges Datums-/Zeitformat"
+ );
+ return;
+ }
+ $trip{$key} = $datetime;
+ }
+ }
+
+ for my $key (qw(dep_station arr_station route comment)) {
+ $trip{$key} = $self->param($key);
+ }
+
+ $opt{backend_id} = $self->current_user->{backend_id};
+
+ my $dep_stop = $self->stations->search( $trip{dep_station},
+ backend_id => $opt{backend_id} );
+ my $arr_stop = $self->stations->search( $trip{arr_station},
+ backend_id => $opt{backend_id} );
+
+ if ( defined $trip{route} ) {
+ $trip{route} = [ split( qr{\r?\n\r?}, $trip{route} ) ];
+ }
+
+ my $route_has_start = 0;
+ my $route_has_stop = 0;
+
+ for my $station ( @{ $trip{route} || [] } ) {
+ if ( $station eq $dep_stop->{name}
+ or $station eq $dep_stop->{eva} )
+ {
+ $route_has_start = 1;
+ }
+ if ( $station eq $arr_stop->{name}
+ or $station eq $arr_stop->{eva} )
+ {
+ $route_has_stop = 1;
+ }
+ }
+
+ my @route;
+
+ if ( not $route_has_start ) {
+ push(
+ @route,
+ [
+ $dep_stop->{name},
+ $dep_stop->{eva},
+ {
+ lat => $dep_stop->{lat},
+ lon => $dep_stop->{lon},
+ }
+ ]
+ );
+ }
+
+ if ( $trip{route} ) {
+ my @unknown_stations;
+ my $prev_epoch;
+ for my $station ( @{ $trip{route} } ) {
+ my $ts;
+ my %station_data;
+ if ( $station
+ =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x
+ )
+ {
+ $station = $+{stop};
+ $ts = $parser->parse_datetime( $+{timestamp} );
+ if ( $ts and $ts->epoch > $prev_epoch ) {
+ $station_data{sched_arr} = $ts->epoch;
+ $station_data{sched_dep} = $ts->epoch;
+ $prev_epoch = $ts->epoch;
+ }
+ else {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => "Ungültige Zeitangabe: $+{timestamp}"
+ );
+ return;
+ }
+ }
+ my $station_info = $self->stations->search( $station,
+ backend_id => $opt{backend_id} );
+ if ($station_info) {
+ $station_data{lat} = $station_info->{lat};
+ $station_data{lon} = $station_info->{lon};
+ push(
+ @route,
+ [
+ $station_info->{name}, $station_info->{eva},
+ \%station_data,
+ ]
+ );
+ }
+ else {
+ push( @route, [ $station, undef, {} ] );
+ push( @unknown_stations, $station );
+ }
+ }
+
+ if ( @unknown_stations == 1 ) {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => "Unbekannter Unterwegshalt: $unknown_stations[0]"
+ );
+ return;
+ }
+ elsif (@unknown_stations) {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => 'Unbekannte Unterwegshalte: '
+ . join( ', ', @unknown_stations )
+ );
+ return;
+ }
+ }
+
+ if ( not $route_has_stop ) {
+ push(
+ @route,
+ [
+ $arr_stop->{name},
+ $arr_stop->{eva},
+ {
+ lat => $arr_stop->{lat},
+ lon => $arr_stop->{lon},
+ }
+ ]
+ );
+ }
+
+ for my $station (@route) {
+ if ( $station->[0] eq $dep_stop->{name}
+ or $station->[1] eq $dep_stop->{eva} )
+ {
+ $station->[2]{sched_dep} = $trip{sched_departure}->epoch;
+ }
+ if ( $station->[0] eq $arr_stop->{name}
+ or $station->[1] eq $arr_stop->{eva} )
+ {
+ $station->[2]{sched_arr} = $trip{sched_arrival}->epoch;
+ }
+ }
+
+ my $error;
+ my $db = $self->pg->db;
+ my $tx = $db->begin;
+
+ $trip{dep_id} = $dep_stop->{eva};
+ $trip{arr_id} = $arr_stop->{eva};
+ $trip{route} = \@route;
+
+ $opt{db} = $db;
+ $opt{manual} = \%trip;
+ $opt{uid} = $self->current_user->{id};
+
+ if ( not defined $trip{dep_id} ) {
+ $error = "Unknown departure stop '$trip{dep_station}'";
+ }
+ elsif ( not defined $trip{arr_id} ) {
+ $error = "Unknown arrival stop '$trip{arr_station}'";
+ }
+ elsif ( $trip{sched_arrival} <= $trip{sched_departure} ) {
+ $error = 'Ankunftszeit muss nach Abfahrtszeit liegen';
+ }
+ else {
+ $error = $self->in_transit->add(%opt);
+ }
+
+ if ($error) {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ status => 400,
+ error => $error,
+ );
+ }
+ else {
+ $tx->commit;
+ $self->redirect_to('/');
+ }
+ }
+ else {
+ $self->render(
+ 'add_intransit',
+ with_autocomplete => 1,
+ error => undef
+ );
+ }
+}
+
1;
diff --git a/lib/Travelynx/Helper/DBDB.pm b/lib/Travelynx/Helper/DBDB.pm
index b98a372..a310aa3 100644
--- a/lib/Travelynx/Helper/DBDB.pm
+++ b/lib/Travelynx/Helper/DBDB.pm
@@ -27,61 +27,102 @@ sub new {
}
sub has_wagonorder_p {
- my ( $self, $ts, $train_no ) = @_;
- my $api_ts = $ts->strftime('%Y%m%d%H%M');
- my $url
- = "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}";
- my $cache = $self->{cache};
+ my ( $self, %opt ) = @_;
+
+ $opt{train_type} //= q{};
+ my $datetime = $opt{datetime}->clone->set_time_zone('UTC');
+ my %param = (
+ administrationId => 80,
+ category => $opt{train_type},
+ date => $datetime->strftime('%Y-%m-%d'),
+ evaNumber => $opt{eva},
+ number => $opt{train_no},
+ 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;
+ my $debug_prefix
+ = "has_wagonorder_p($opt{train_type} $opt{train_no} @ $opt{eva})";
- if ( my $content = $cache->get("HEAD $url") ) {
+ if ( my $content = $self->{main_cache}->get("HEAD $url")
+ // $self->{realtime_cache}->get("HEAD $url") )
+ {
if ( $content eq 'n' ) {
+ $self->{log}->debug("${debug_prefix}: n (cached)");
return $promise->reject;
}
else {
+ $self->{log}->debug("${debug_prefix}: ${content} (cached)");
return $promise->resolve($content);
}
}
- $self->{user_agent}->request_timeout(5)->head_p( $url => $self->{header} )
+ $self->{user_agent}->request_timeout(5)
+ ->get_p( $url => $self->{header} )
->then(
sub {
my ($tx) = @_;
if ( $tx->result->is_success ) {
- $cache->set( "HEAD $url", 'a' );
+ $self->{log}->debug("${debug_prefix}: a");
+ $self->{main_cache}->set( "HEAD $url", 'a' );
+ my $body = decode( 'utf-8', $tx->res->body );
+ my $json = JSON->new->decode($body);
+ $self->{main_cache}->freeze( $url, $json );
$promise->resolve('a');
}
else {
- $cache->set( "HEAD $url", 'n' );
+ my $code = $tx->res->code;
+ $self->{log}->debug("${debug_prefix}: n (HTTP $code)");
+ $self->{realtime_cache}->set( "HEAD $url", 'n' );
$promise->reject;
}
return;
}
- )->catch(
+ )->catch(
sub {
- $cache->set( "HEAD $url", 'n' );
+ my ($err) = @_;
+ $self->{log}->debug("${debug_prefix}: n ($err)");
+ $self->{realtime_cache}->set( "HEAD $url", 'n' );
$promise->reject;
return;
}
- )->wait;
+ )->wait;
return $promise;
}
sub get_wagonorder_p {
- my ( $self, $api, $ts, $train_no ) = @_;
- my $api_ts = $ts->strftime('%Y%m%d%H%M');
- my $url
- = "https://ist-wr.noncd.db.de/wagenreihung/1.0/${train_no}/${api_ts}";
+ my ( $self, %opt ) = @_;
+
+ my $datetime = $opt{datetime}->clone->set_time_zone('UTC');
+ my %param = (
+ administrationId => 80,
+ category => $opt{train_type},
+ date => $datetime->strftime('%Y-%m-%d'),
+ evaNumber => $opt{eva},
+ number => $opt{train_no},
+ 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 $debug_prefix
+ = "get_wagonorder_p($opt{train_type} $opt{train_no} @ $opt{eva})";
- my $cache = $self->{cache};
my $promise = Mojo::Promise->new;
- if ( my $content = $cache->thaw($url) ) {
+ if ( my $content = $self->{main_cache}->thaw($url) ) {
+ $self->{log}->debug("${debug_prefix}: (cached)");
$promise->resolve($content);
return $promise;
}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
+ $self->{user_agent}->request_timeout(5)
+ ->get_p( $url => $self->{header} )
->then(
sub {
my ($tx) = @_;
@@ -89,22 +130,25 @@ sub get_wagonorder_p {
if ( $tx->result->is_success ) {
my $body = decode( 'utf-8', $tx->res->body );
my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
+ $self->{log}->debug("${debug_prefix}: success");
+ $self->{main_cache}->freeze( $url, $json );
$promise->resolve($json);
}
else {
- my $code = $tx->code;
+ my $code = $tx->res->code;
+ $self->{log}->debug("${debug_prefix}: HTTP ${code}");
$promise->reject("HTTP ${code}");
}
return;
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
+ $self->{log}->debug("${debug_prefix}: error ${err}");
$promise->reject($err);
return;
}
- )->wait;
+ )->wait;
return $promise;
}
@@ -113,37 +157,44 @@ sub get_stationinfo_p {
my $url = "https://lib.finalrewind.org/dbdb/s/${eva}.json";
- my $cache = $self->{cache};
+ my $cache = $self->{main_cache};
my $promise = Mojo::Promise->new;
if ( my $content = $cache->thaw($url) ) {
+ $self->{log}->debug("get_stationinfo_p(${eva}): (cached)");
return $promise->resolve($content);
}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
+ $self->{user_agent}->request_timeout(5)
+ ->get_p( $url => $self->{header} )
->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
+ $self->{log}->debug(
+"get_stationinfo_p(${eva}): HTTP $err->{code} $err->{message}"
+ );
$cache->freeze( $url, {} );
$promise->reject("HTTP $err->{code} $err->{message}");
return;
}
my $json = $tx->result->json;
+ $self->{log}->debug("get_stationinfo_p(${eva}): success");
$cache->freeze( $url, $json );
$promise->resolve($json);
return;
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
+ $self->{log}->debug("get_stationinfo_p(${eva}): Error ${err}");
$cache->freeze( $url, {} );
$promise->reject($err);
return;
}
- )->wait;
+ )->wait;
return $promise;
}
diff --git a/lib/Travelynx/Helper/DBRIS.pm b/lib/Travelynx/Helper/DBRIS.pm
new file mode 100644
index 0000000..1b7f099
--- /dev/null
+++ b/lib/Travelynx/Helper/DBRIS.pm
@@ -0,0 +1,146 @@
+package Travelynx::Helper::DBRIS;
+
+# Copyright (C) 2025 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+use utf8;
+
+use DateTime;
+use Encode qw(decode);
+use JSON;
+use Mojo::Promise;
+use Mojo::UserAgent;
+use Travel::Status::DE::DBRIS;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ my $version = $opt{version};
+
+ $opt{header}
+ = { 'User-Agent' =>
+"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx"
+ };
+
+ return bless( \%opt, $class );
+}
+
+sub get_station_id_p {
+ my ( $self, $station_name ) = @_;
+ my $promise = Mojo::Promise->new;
+ Travel::Status::DE::DBRIS->new_p(
+ locationSearch => $station_name,
+ cache => $self->{cache},
+ lwp_options => {
+ timeout => 10,
+ agent => $self->{header}{'User-Agent'},
+ },
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ )->then(
+ sub {
+ my ($dbris) = @_;
+ my $found;
+ for my $result ( $dbris->results ) {
+ if ( defined $result->eva ) {
+ $promise->resolve($result);
+ return;
+ }
+ }
+ $promise->reject("Unable to find station '$station_name'");
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject("'$err' while trying to look up '$station_name'");
+ return;
+ }
+ )->wait;
+ return $promise;
+}
+
+sub get_departures_p {
+ my ( $self, %opt ) = @_;
+
+ my $agent = $self->{user_agent};
+
+ if ( $opt{station} =~ m{ [@] L = (?<eva> \d+ ) }x ) {
+ $opt{station} = {
+ eva => $+{eva},
+ id => $opt{station},
+ };
+ }
+
+ my $when = (
+ $opt{timestamp}
+ ? $opt{timestamp}->clone
+ : DateTime->now( time_zone => 'Europe/Berlin' )
+ )->subtract( minutes => $opt{lookbehind} );
+ return Travel::Status::DE::DBRIS->new_p(
+ station => $opt{station},
+ datetime => $when,
+ cache => $self->{cache},
+ promise => 'Mojo::Promise',
+ user_agent => $agent->request_timeout(10),
+ );
+}
+
+sub get_journey_p {
+ my ( $self, %opt ) = @_;
+
+ my $promise = Mojo::Promise->new;
+
+ my $agent = $self->{user_agent};
+ my $proxy;
+ if ( my @proxies = @{ $self->{service_config}{'bahn.de'}{proxies} // [] } )
+ {
+ $proxy = $proxies[ int( rand( scalar @proxies ) ) ];
+ }
+ elsif ( my $p = $self->{service_config}{'bahn.de'}{proxy} ) {
+ $proxy = $p;
+ }
+
+ if ($proxy) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
+ Travel::Status::DE::DBRIS->new_p(
+ journey => $opt{trip_id},
+ with_polyline => $opt{with_polyline},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $agent->request_timeout(10),
+ )->then(
+ sub {
+ my ($dbris) = @_;
+ my $journey = $dbris->result;
+
+ if ($journey) {
+ $self->{log}->debug("get_journey_p($opt{trip_id}): success");
+ $promise->resolve($journey);
+ return;
+ }
+ $self->{log}->debug("get_journey_p($opt{trip_id}): no journey");
+ $promise->reject('no journey');
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->debug("get_journey_p($opt{trip_id}): error $err");
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+1;
diff --git a/lib/Travelynx/Helper/EFA.pm b/lib/Travelynx/Helper/EFA.pm
new file mode 100644
index 0000000..5cae51b
--- /dev/null
+++ b/lib/Travelynx/Helper/EFA.pm
@@ -0,0 +1,105 @@
+package Travelynx::Helper::EFA;
+
+# Copyright (C) 2024 Birte Kristina Friesel
+#
+# SPDX-License-Identifier: AGPL-3.0-or-later
+
+use strict;
+use warnings;
+use 5.020;
+
+use Travel::Status::DE::EFA;
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ my $version = $opt{version};
+
+ $opt{header}
+ = { 'User-Agent' =>
+"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx"
+ };
+
+ return bless( \%opt, $class );
+}
+
+sub get_service {
+ my ( $self, $service ) = @_;
+
+ return Travel::Status::DE::EFA::get_service($service);
+}
+
+sub get_departures_p {
+ my ( $self, %opt ) = @_;
+
+ my $when = (
+ $opt{timestamp}
+ ? $opt{timestamp}->clone
+ : DateTime->now( time_zone => 'Europe/Berlin' )
+ )->subtract( minutes => $opt{lookbehind} );
+ return Travel::Status::DE::EFA->new_p(
+ service => $opt{service},
+ name => $opt{name},
+ datetime => $when,
+ full_routes => 1,
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $self->{user_agent}->request_timeout(5),
+ );
+}
+
+sub get_journey_p {
+ my ( $self, %opt ) = @_;
+
+ my $promise = Mojo::Promise->new;
+ my $agent = $self->{user_agent};
+ my $stopseq;
+
+ if ( $opt{trip_id}
+ =~ m{ ^ ([^@]*) @ ([^@]*) [(] ([^T]*) T ([^)]*) [)] (.*) $ }x )
+ {
+ $stopseq = {
+ stateless => $1,
+ stop_id => $2,
+ date => $3,
+ time => $4,
+ key => $5
+ };
+ }
+ else {
+ return $promise->reject("Invalid trip_id: $opt{trip_id}");
+ }
+
+ Travel::Status::DE::EFA->new_p(
+ service => $opt{service},
+ stopseq => $stopseq,
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => $agent->request_timeout(10),
+ )->then(
+ sub {
+ my ($efa) = @_;
+ my $journey = $efa->result;
+
+ if ($journey) {
+ $self->{log}->debug("get_journey_p($opt{trip_id}): success");
+ $promise->resolve($journey);
+ return;
+ }
+ $self->{log}->debug("get_journey_p($opt{trip_id}): no journey");
+ $promise->reject('no journey');
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->debug("get_journey_p($opt{trip_id}): error $err");
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+1;
diff --git a/lib/Travelynx/Helper/HAFAS.pm b/lib/Travelynx/Helper/HAFAS.pm
index 7671d78..c35dfdb 100644
--- a/lib/Travelynx/Helper/HAFAS.pm
+++ b/lib/Travelynx/Helper/HAFAS.pm
@@ -7,11 +7,13 @@ package Travelynx::Helper::HAFAS;
use strict;
use warnings;
use 5.020;
+use utf8;
use DateTime;
use Encode qw(decode);
use JSON;
use Mojo::Promise;
+use Mojo::UserAgent;
use Travel::Status::DE::HAFAS;
sub _epoch {
@@ -33,82 +35,73 @@ sub new {
return bless( \%opt, $class );
}
-sub get_json_p {
- my ( $self, $url, %opt ) = @_;
+sub class_to_product {
+ my ( $self, $hafas ) = @_;
- my $cache = $self->{main_cache};
- my $promise = Mojo::Promise->new;
-
- if ( $opt{realtime} ) {
- $cache = $self->{realtime_cache};
- }
- $opt{encoding} //= 'ISO-8859-15';
+ my $bits = $hafas->get_active_service->{productbits};
+ my $ret;
- if ( my $content = $cache->thaw($url) ) {
- return $promise->resolve($content);
+ for my $i ( 0 .. $#{$bits} ) {
+ $ret->{ 2**$i }
+ = ref( $bits->[$i] ) eq 'ARRAY' ? $bits->[$i][0] : $bits->[$i];
}
- $self->{user_agent}->request_timeout(5)->get_p( $url => $self->{header} )
- ->then(
- sub {
- my ($tx) = @_;
-
- if ( my $err = $tx->error ) {
- $promise->reject(
-"hafas->get_json_p($url) returned HTTP $err->{code} $err->{message}"
- );
- return;
- }
+ return $ret;
+}
- my $body = decode( $opt{encoding}, $tx->res->body );
+sub get_service {
+ my ( $self, $service ) = @_;
- $body =~ s{^TSLs[.]sls = }{};
- $body =~ s{;$}{};
- $body =~ s{&#x0028;}{(}g;
- $body =~ s{&#x0029;}{)}g;
- my $json = JSON->new->decode($body);
- $cache->freeze( $url, $json );
- $promise->resolve($json);
- return;
- }
- )->catch(
- sub {
- my ($err) = @_;
- $self->{log}->info("hafas->get_json_p($url): $err");
- $promise->reject("hafas->get_json_p($url): $err");
- return;
- }
- )->wait;
- return $promise;
+ return Travel::Status::DE::HAFAS::get_service($service);
}
sub get_departures_p {
my ( $self, %opt ) = @_;
+ $opt{service} //= 'ÖBB';
+
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
my $when = (
$opt{timestamp}
? $opt{timestamp}->clone
: DateTime->now( time_zone => 'Europe/Berlin' )
)->subtract( minutes => $opt{lookbehind} );
return Travel::Status::DE::HAFAS->new_p(
+ service => $opt{service},
station => $opt{eva},
datetime => $when,
lookahead => $opt{lookahead} + $opt{lookbehind},
results => 300,
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(5),
+ user_agent => $agent->request_timeout(5),
);
}
sub search_location_p {
my ( $self, %opt ) = @_;
+ $opt{service} //= 'ÖBB';
+
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
return Travel::Status::DE::HAFAS->new_p(
+ service => $opt{service},
locationSearch => $opt{query},
cache => $self->{realtime_cache},
promise => 'Mojo::Promise',
- user_agent => $self->{user_agent}->request_timeout(5),
+ user_agent => $agent->request_timeout(5),
);
}
@@ -121,23 +114,36 @@ sub get_tripid_p {
my $train_desc = $train->type . ' ' . $train->train_no;
$train_desc =~ s{^- }{};
+ $opt{service} //= 'ÖBB';
+
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
Travel::Status::DE::HAFAS->new_p(
+ service => $opt{service},
journeyMatch => $train_desc,
datetime => $train->start,
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) = @_;
my @results = $hafas->results;
if ( not @results ) {
+ $self->{log}->debug("get_tripid_p($train_desc): no results");
$promise->reject(
"journeyMatch($train_desc) returned no results");
return;
}
+ $self->{log}->debug("get_tripid_p($train_desc): success");
+
my $result = $results[0];
if ( @results > 1 ) {
for my $journey (@results) {
@@ -154,6 +160,7 @@ sub get_tripid_p {
)->catch(
sub {
my ($err) = @_;
+ $self->{log}->debug("get_tripid_p($train_desc): error $err");
$promise->reject($err);
return;
}
@@ -168,29 +175,42 @@ sub get_journey_p {
my $promise = Mojo::Promise->new;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ $opt{service} //= 'ÖBB';
+
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
Travel::Status::DE::HAFAS->new_p(
+ service => $opt{service},
journey => {
id => $opt{trip_id},
},
with_polyline => $opt{with_polyline},
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) = @_;
my $journey = $hafas->result;
if ($journey) {
+ $self->{log}->debug("get_journey_p($opt{trip_id}): success");
$promise->resolve($journey);
return;
}
+ $self->{log}->debug("get_journey_p($opt{trip_id}): no journey");
$promise->reject('no journey');
return;
}
)->catch(
sub {
my ($err) = @_;
+ $self->{log}->debug("get_journey_p($opt{trip_id}): error $err");
$promise->reject($err);
return;
}
@@ -199,13 +219,23 @@ sub get_journey_p {
return $promise;
}
-sub get_route_timestamps_p {
+sub get_route_p {
my ( $self, %opt ) = @_;
my $promise = Mojo::Promise->new;
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
+ $opt{service} //= 'ÖBB';
+
+ my $agent = $self->{user_agent};
+ if ( my $proxy = $self->{service_config}{ $opt{service} }{proxy} ) {
+ $agent = Mojo::UserAgent->new;
+ $agent->proxy->http($proxy);
+ $agent->proxy->https($proxy);
+ }
+
Travel::Status::DE::HAFAS->new_p(
+ service => $opt{service},
journey => {
id => $opt{trip_id},
@@ -214,18 +244,17 @@ sub get_route_timestamps_p {
with_polyline => $opt{with_polyline},
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) = @_;
my $journey = $hafas->result;
- my $ret = {};
+ my $ret = [];
my $polyline;
my $station_is_past = 1;
for my $stop ( $journey->route ) {
- my $name = $stop->loc->name;
- $ret->{$name} = $ret->{ $stop->loc->eva } = {
+ my $entry = {
name => $stop->loc->name,
eva => $stop->loc->eva,
sched_arr => _epoch( $stop->sched_arr ),
@@ -234,29 +263,32 @@ sub get_route_timestamps_p {
rt_dep => _epoch( $stop->rt_dep ),
arr_delay => $stop->arr_delay,
dep_delay => $stop->dep_delay,
- load => $stop->load
+ load => $stop->load,
+ lat => $stop->loc->lat,
+ lon => $stop->loc->lon,
};
if ( $stop->tz_offset ) {
- $ret->{$name}{tz_offset} = $stop->tz_offset;
+ $entry->{tz_offset} = $stop->tz_offset;
}
if ( ( $stop->arr_cancelled or not $stop->sched_arr )
and ( $stop->dep_cancelled or not $stop->sched_dep ) )
{
- $ret->{$name}{isCancelled} = 1;
+ $entry->{isCancelled} = 1;
}
if (
$station_is_past
- and not $ret->{$name}{isCancelled}
+ and not $entry->{isCancelled}
and $now->epoch < (
- $ret->{$name}{rt_arr} // $ret->{$name}{rt_dep}
- // $ret->{$name}{sched_arr}
- // $ret->{$name}{sched_dep} // $now->epoch
+ $entry->{rt_arr} // $entry->{rt_dep}
+ // $entry->{sched_arr} // $entry->{sched_dep}
+ // $now->epoch
)
)
{
$station_is_past = 0;
}
- $ret->{$name}{isPast} = $station_is_past;
+ $entry->{isPast} = $station_is_past;
+ push( @{$ret}, $entry );
}
if ( $journey->polyline ) {
@@ -298,12 +330,14 @@ sub get_route_timestamps_p {
}
}
+ $self->{log}->debug("get_route_p($opt{trip_id}): success");
$promise->resolve( $ret, $journey, $polyline );
return;
}
)->catch(
sub {
my ($err) = @_;
+ $self->{log}->debug("get_route_p($opt{trip_id}): error $err");
$promise->reject($err);
return;
}
diff --git a/lib/Travelynx/Helper/IRIS.pm b/lib/Travelynx/Helper/IRIS.pm
index deed79a..34739eb 100644
--- a/lib/Travelynx/Helper/IRIS.pm
+++ b/lib/Travelynx/Helper/IRIS.pm
@@ -41,8 +41,12 @@ sub get_departures {
my @station_matches
= Travel::Status::DE::IRIS::Stations::get_station($station);
+ if ( $station =~ m{ ^ \d+ $ }x ) {
+ @station_matches = ( [ undef, undef, $station ] );
+ }
+
if ( @station_matches == 1 ) {
- $station = $station_matches[0][0];
+ $station = $station_matches[0][2];
my $status = Travel::Status::DE::IRIS->new(
station => $station,
main_cache => $self->{main_cache},
@@ -108,8 +112,12 @@ sub get_departures_p {
my @station_matches
= Travel::Status::DE::IRIS::Stations::get_station($station);
+ if ( $station =~ m{ ^ \d+ $ }x ) {
+ @station_matches = ( [ undef, undef, $station ] );
+ }
+
if ( @station_matches == 1 ) {
- $station = $station_matches[0][0];
+ $station = $station_matches[0][2];
my $promise = Mojo::Promise->new;
Travel::Status::DE::IRIS->new_p(
station => $station,
diff --git a/lib/Travelynx/Helper/MOTIS.pm b/lib/Travelynx/Helper/MOTIS.pm
new file mode 100644
index 0000000..df79385
--- /dev/null
+++ b/lib/Travelynx/Helper/MOTIS.pm
@@ -0,0 +1,161 @@
+package Travelynx::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 utf8;
+
+use DateTime;
+use Encode qw(decode);
+use JSON;
+use Mojo::Promise;
+use Mojo::UserAgent;
+
+use Travel::Status::MOTIS;
+
+sub _epoch {
+ my ($dt) = @_;
+
+ return $dt ? $dt->epoch : 0;
+}
+
+sub new {
+ my ( $class, %opt ) = @_;
+
+ my $version = $opt{version};
+
+ $opt{header}
+ = { 'User-Agent' =>
+"travelynx/${version} on $opt{root_url} +https://finalrewind.org/projects/travelynx"
+ };
+
+ return bless( \%opt, $class );
+}
+
+sub get_service {
+ my ( $self, $service ) = @_;
+
+ return Travel::Status::MOTIS::get_service($service);
+}
+
+sub get_station_by_query_p {
+ my ( $self, %opt ) = @_;
+
+ $opt{service} //= 'transitous';
+
+ my $promise = Mojo::Promise->new;
+
+ Travel::Status::MOTIS->new_p(
+ cache => $self->{cache},
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ time_zone => 'Europe/Berlin',
+ lwp_options => {
+ timeout => 10,
+ agent => $self->{header}{'User-Agent'},
+ },
+
+ service => $opt{service},
+ stops_by_query => $opt{query},
+ )->then(
+ sub {
+ my ($motis) = @_;
+ my $found;
+
+ for my $result ( $motis->results ) {
+ if ( defined $result->id ) {
+ $promise->resolve($result);
+ return;
+ }
+ }
+
+ $promise->reject("Unable to find station '$opt{query}'");
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $promise->reject("'$err' while trying to look up '$opt{query}'");
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+sub get_departures_p {
+ my ( $self, %opt ) = @_;
+
+ $opt{service} //= 'transitous';
+
+ my $timestamp = (
+ $opt{timestamp}
+ ? $opt{timestamp}->clone
+ : DateTime->now
+ )->subtract( minutes => $opt{lookbehind} );
+
+ return Travel::Status::MOTIS->new_p(
+ cache => $self->{cache},
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ time_zone => 'Europe/Berlin',
+ lwp_options => {
+ timeout => 10,
+ agent => $self->{header}{'User-Agent'},
+ },
+
+ service => $opt{service},
+ timestamp => $timestamp,
+ stop_id => $opt{station_id},
+ results => 60,
+ );
+}
+
+sub get_trip_p {
+ my ( $self, %opt ) = @_;
+
+ $opt{service} //= 'transitous';
+
+ my $promise = Mojo::Promise->new;
+
+ Travel::Status::MOTIS->new_p(
+ with_polyline => $opt{with_polyline},
+ cache => $self->{realtime_cache},
+ promise => 'Mojo::Promise',
+ user_agent => Mojo::UserAgent->new,
+ time_zone => 'Europe/Berlin',
+
+ service => $opt{service},
+ trip_id => $opt{trip_id},
+ )->then(
+ sub {
+ my ($motis) = @_;
+ my $journey = $motis->result;
+
+ if ($journey) {
+ $self->{log}->debug("get_trip_p($opt{trip_id}): success");
+ $promise->resolve($journey);
+ return;
+ }
+
+ $self->{log}->debug("get_trip_p($opt{trip_id}): no journey");
+ $promise->reject('no journey');
+ return;
+ }
+ )->catch(
+ sub {
+ my ($err) = @_;
+ $self->{log}->debug("get_trip_p($opt{trip_id}): error $err");
+ $promise->reject($err);
+ return;
+ }
+ )->wait;
+
+ return $promise;
+}
+
+1;
diff --git a/lib/Travelynx/Helper/Sendmail.pm b/lib/Travelynx/Helper/Sendmail.pm
index baa1156..54829c8 100644
--- a/lib/Travelynx/Helper/Sendmail.pm
+++ b/lib/Travelynx/Helper/Sendmail.pm
@@ -9,7 +9,7 @@ use warnings;
use 5.020;
-use Encode qw(encode);
+use Encode qw(encode);
use Email::Sender::Simple qw(try_to_sendmail);
use MIME::Entity;
diff --git a/lib/Travelynx/Helper/Traewelling.pm b/lib/Travelynx/Helper/Traewelling.pm
index d688004..66f2a29 100644
--- a/lib/Travelynx/Helper/Traewelling.pm
+++ b/lib/Travelynx/Helper/Traewelling.pm
@@ -78,7 +78,8 @@ sub get_status_p {
$self->{user_agent}->request_timeout(20)
->get_p(
"https://traewelling.de/api/v1/user/${username}/statuses?limit=1" =>
- $header )->then(
+ $header )
+ ->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
@@ -116,6 +117,7 @@ sub get_status_p {
my $category = $status->{train}{category};
my $linename = $status->{train}{lineName};
+ my $train_no = $status->{train}{journeyNumber};
my $trip_id = $status->{train}{hafasId};
my ( $train_type, $train_line ) = split( qr{ }, $linename );
$promise->resolve(
@@ -133,6 +135,7 @@ sub get_status_p {
arr_ds100 => $arr_ds100,
arr_name => $arr_name,
trip_id => $trip_id,
+ train_no => $train_no,
train_type => $train_type,
line => $linename,
line_no => $train_line,
@@ -148,13 +151,13 @@ sub get_status_p {
}
}
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$promise->reject( { text => "v1/${username}/statuses: $err" } );
return;
}
- )->wait;
+ )->wait;
return $promise;
}
@@ -236,13 +239,13 @@ sub logout_p {
return;
}
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$promise->reject("v1/auth/logout: $err");
return;
}
- )->wait;
+ )->wait;
return $promise;
}
@@ -320,7 +323,8 @@ sub checkin_p {
$self->{user_agent}->request_timeout(20)
->post_p(
"https://traewelling.de/api/v1/trains/checkin" => $header => json =>
- $request )->then(
+ $request )
+ ->then(
sub {
my ($tx) = @_;
if ( my $err = $tx->error ) {
@@ -366,7 +370,7 @@ sub checkin_p {
# on the user status page
return;
}
- )->catch(
+ )->catch(
sub {
my ($err) = @_;
$self->{log}->debug("... $debug_prefix error: $err");
@@ -379,7 +383,7 @@ sub checkin_p {
$promise->reject( { connection => $err } );
return;
}
- )->wait;
+ )->wait;
return $promise;
}
diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm
index 69026ac..11177dd 100644
--- a/lib/Travelynx/Model/InTransit.pm
+++ b/lib/Travelynx/Model/InTransit.pm
@@ -1,6 +1,7 @@
package Travelynx::Model::InTransit;
-# Copyright (C) 2020-2023 Birte Kristina Friesel
+# Copyright (C) 2020-2025 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -12,11 +13,12 @@ use DateTime;
use JSON;
my %visibility_itoa = (
- 100 => 'public',
- 80 => 'travelynx',
- 60 => 'followers',
- 30 => 'unlisted',
- 10 => 'private',
+ 100 => 'public',
+ 80 => 'travelynx',
+ 60 => 'followers',
+ 30 => 'unlisted',
+ 10 => 'private',
+ default => 'default',
);
my %visibility_atoi = (
@@ -30,7 +32,7 @@ my %visibility_atoi = (
sub _epoch {
my ($dt) = @_;
- return $dt ? $dt->epoch : 0;
+ return $dt ? $dt->epoch : undef;
}
sub epoch_to_dt {
@@ -47,6 +49,16 @@ sub epoch_to_dt {
);
}
+sub epoch_or_dt_to_dt {
+ my ($input) = @_;
+
+ if ( ref($input) eq 'DateTime' ) {
+ return $input;
+ }
+
+ return epoch_to_dt($input);
+}
+
sub new {
my ( $class, %opt ) = @_;
@@ -83,13 +95,20 @@ sub add {
my $uid = $opt{uid};
my $db = $opt{db} // $self->{pg}->db;
+ my $backend_id = $opt{backend_id};
my $train = $opt{train};
+ my $train_suffix = $opt{train_suffix};
my $journey = $opt{journey};
my $stop = $opt{stop};
+ my $stopover = $opt{stopover};
+ my $manual = $opt{manual};
my $checkin_station_id = $opt{departure_eva};
my $route = $opt{route};
+ my $data = $opt{data};
+ my $persistent_data;
my $json = JSON->new;
+ my $now = DateTime->now( time_zone => 'Europe/Berlin' );
if ($train) {
$db->insert(
@@ -99,28 +118,86 @@ sub add {
cancelled => $train->departure_is_cancelled ? 1
: 0,
checkin_station_id => $checkin_station_id,
- checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ),
- dep_platform => $train->platform,
- train_type => $train->type,
- train_line => $train->line_no,
- train_no => $train->train_no,
- train_id => $train->train_id,
- sched_departure => $train->sched_departure,
- real_departure => $train->departure,
- route => $json->encode($route),
- messages => $json->encode(
+ checkin_time => $now,
+ dep_platform => $train->platform,
+ train_type => $train->type,
+ train_line => $train->line_no,
+ train_no => $train->train_no,
+ train_id => $train->train_id,
+ sched_departure => $train->sched_departure,
+ real_departure => $train->departure,
+ route => $json->encode($route),
+ messages => $json->encode(
[ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ]
),
- data => JSON->new->encode(
+ data => $json->encode(
{
rt => $train->departure_has_realtime ? 1
- : 0
+ : 0,
+ %{ $data // {} }
}
),
+ backend_id => $backend_id,
}
);
}
- elsif ( $journey and $stop ) {
+ elsif ( $journey
+ and $stop
+ and ref($journey) eq 'Travel::Status::DE::EFA::Trip' )
+ {
+ my @route;
+ for my $j_stop ( $journey->route ) {
+ push(
+ @route,
+ [
+ $j_stop->full_name,
+ $j_stop->id_num,
+ {
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ isCancelled => $j_stop->is_cancelled,
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ efa_load => $j_stop->occupancy,
+ lat => $j_stop->latlon->[0],
+ lon => $j_stop->latlon->[1],
+ }
+ ]
+ );
+ }
+ $persistent_data->{operator} = $journey->operator;
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => $stop->is_cancelled ? 1 : 0,
+ checkin_station_id => $stop->id_num,
+ checkin_time => $now,
+ dep_platform => $stop->platform,
+ train_type => $journey->type // q{},
+ train_line => $journey->line,
+ train_no => $journey->number // q{},
+ train_id => $opt{trip_id},
+ sched_departure => $stop->sched_dep,
+ real_departure => $stop->rt_dep // $stop->sched_dep,
+ route => $json->encode( \@route ),
+ data => $json->encode(
+ {
+ rt => $stop->rt_dep ? 1 : 0,
+ %{ $data // {} }
+ }
+ ),
+ user_data => $json->encode($persistent_data),
+ backend_id => $backend_id,
+ }
+ );
+ }
+ elsif ( $journey
+ and $stop
+ and ref($journey) eq 'Travel::Status::DE::HAFAS::Journey' )
+ {
my @route;
my $product = $journey->product_at( $stop->loc->eva )
// $journey->product;
@@ -137,7 +214,9 @@ sub add {
rt_dep => _epoch( $j_stop->rt_dep ),
arr_delay => $j_stop->arr_delay,
dep_delay => $j_stop->dep_delay,
- load => $j_stop->load
+ load => $j_stop->load,
+ lat => $j_stop->loc->lat,
+ lon => $j_stop->loc->lon,
}
]
);
@@ -145,6 +224,9 @@ sub add {
$route[-1][2]{tz_offset} = $j_stop->tz_offset;
}
}
+ if ( scalar $journey->operators ) {
+ $persistent_data->{operators} = [ $journey->operators ];
+ }
$db->insert(
'in_transit',
{
@@ -153,21 +235,207 @@ sub add {
? 1
: 0,
checkin_station_id => $stop->loc->eva,
+ checkin_time => $now,
+ dep_platform => $stop->{platform},
+ train_type => $product->type // q{},
+ train_line => $product->line_no,
+ train_no => $product->number // q{},
+ train_id => $journey->id,
+ sched_departure => $stop->{sched_dep},
+ real_departure => $stop->{rt_dep} // $stop->{sched_dep},
+ route => $json->encode( \@route ),
+ data => $json->encode(
+ {
+ rt => $stop->{rt_dep} ? 1 : 0,
+ %{ $data // {} }
+ }
+ ),
+ user_data => $json->encode($persistent_data),
+ backend_id => $backend_id,
+ }
+ );
+ }
+ elsif ( $journey
+ and $stop
+ and ref($journey) eq 'Travel::Status::DE::DBRIS::Journey' )
+ {
+ my $number = $journey->train_no // $journey->number // $train_suffix;
+
+ my $line;
+ if ( defined $journey->line_no and $journey->line_no ne $number ) {
+ $line = $journey->line_no;
+ }
+ elsif ( defined $train_suffix and $train_suffix ne $number ) {
+ $line = $train_suffix;
+ }
+
+ my @route;
+ for my $j_stop ( $journey->route ) {
+ push(
+ @route,
+ [
+ $j_stop->name,
+ $j_stop->eva,
+ {
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ isCancelled => $j_stop->is_cancelled,
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ load => {
+ FIRST => $j_stop->occupancy_first,
+ SECOND => $j_stop->occupancy_second
+ },
+ lat => $j_stop->lat,
+ lon => $j_stop->lon,
+ }
+ ]
+ );
+ }
+ my @messages;
+ for my $msg ( $journey->messages ) {
+ if ( not $msg->{ueberschrift} ) {
+ push(
+ @{ $data->{him_msg} },
+ {
+ header => q{},
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ push(
+ @{ $persistent_data->{him_msg} },
+ {
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ }
+ }
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => $stop->is_cancelled
+ ? 1
+ : 0,
+ checkin_station_id => $stop->eva,
+ checkin_time => $now,
+ dep_platform => $stop->platform,
+ train_type => $journey->type // q{},
+ train_line => $line,
+ train_no => $number,
+ train_id => $data->{trip_id},
+ sched_departure => $stop->sched_dep,
+ real_departure => $stop->rt_dep // $stop->sched_dep,
+ route => $json->encode( \@route ),
+ data => $json->encode(
+ {
+ rt => $stop->{rt_dep} ? 1 : 0,
+ %{ $data // {} }
+ }
+ ),
+ user_data => $json->encode($persistent_data),
+ backend_id => $backend_id,
+ }
+ );
+ }
+ elsif ( $journey
+ and $stopover
+ and ref($journey) eq 'Travel::Status::MOTIS::Trip' )
+ {
+ my @route;
+ for my $journey_stopover ( $journey->stopovers ) {
+ push(
+ @route,
+ [
+ $journey_stopover->stop->name,
+ $journey_stopover->stop->{eva}
+ // die('eva not set for stopover'),
+ {
+ sched_arr =>
+ _epoch( $journey_stopover->scheduled_arrival ),
+ sched_dep =>
+ _epoch( $journey_stopover->scheduled_departure ),
+ rt_arr => _epoch( $journey_stopover->realtime_arrival ),
+ rt_dep =>
+ _epoch( $journey_stopover->realtime_departure ),
+ arr_delay => $journey_stopover->arrival_delay,
+ dep_delay => $journey_stopover->departure_delay,
+ lat => $journey_stopover->stop->lat,
+ lon => $journey_stopover->stop->lon,
+ }
+ ]
+ );
+ }
+
+ $persistent_data->{operator} = $journey->agency;
+
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => $stopover->{is_cancelled}
+ ? 1
+ : 0,
+ checkin_station_id => $stopover->stop->{eva},
checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ),
- dep_platform => $stop->{platform},
- train_type => $product->type // q{},
- train_line => $product->line_no,
- train_no => $product->number // q{},
+ dep_platform => $stopover->track,
+ train_type => $journey->mode,
+ train_no => q{},
train_id => $journey->id,
- sched_departure => $stop->{sched_dep},
- real_departure => $stop->{rt_dep} // $stop->{sched_dep},
+ train_line => $journey->route_name,
+ sched_departure => $stopover->scheduled_departure,
+ real_departure => $stopover->departure,
route => $json->encode( \@route ),
- data => JSON->new->encode( { rt => $stop->{rt_dep} ? 1 : 0 } ),
+ data => $json->encode(
+ {
+ rt => $stopover->{is_realtime} ? 1 : 0,
+ %{ $data // {} }
+ }
+ ),
+ user_data => $json->encode($persistent_data),
+ backend_id => $backend_id,
+ }
+ );
+ }
+ elsif ($manual) {
+ if ( $manual->{comment} ) {
+ $persistent_data->{comment} = $manual->{comment};
+ }
+ $db->insert(
+ 'in_transit',
+ {
+ user_id => $uid,
+ cancelled => 0,
+ checkin_station_id => $manual->{dep_id},
+ checkout_station_id => $manual->{arr_id},
+ checkin_time => DateTime->now( time_zone => 'Europe/Berlin' ),
+ train_type => $manual->{train_type},
+ train_no => $manual->{train_no} || q{},
+ train_id => 'manual',
+ train_line => $manual->{train_line} || undef,
+ sched_departure => $manual->{sched_departure},
+ real_departure => $manual->{sched_departure},
+ sched_arrival => $manual->{sched_arrival},
+ real_arrival => $manual->{sched_arrival},
+ route => $json->encode( $manual->{route} // [] ),
+ data => $json->encode(
+ {
+ manual => \1,
+ %{ $data // {} }
+ }
+ ),
+ user_data => $json->encode($persistent_data),
+ backend_id => $backend_id,
}
);
+ return;
}
else {
- die('neither train nor journey specified');
+ die('invalid arguments / argument types passed to InTransit->add');
}
}
@@ -211,8 +479,15 @@ sub postprocess {
if ($is_after) {
push( @route_after, $station );
}
- if ( $ret->{dep_name}
- and $station->[0] eq $ret->{dep_name} )
+
+ # Note that the departure stop may be present more than once in @route,
+ # e.g. when traveling along ring lines such as S41 / S42 in Berlin.
+ if (
+ $ret->{dep_name}
+ and $station->[0] eq $ret->{dep_name}
+ and not($station->[2]{sched_dep}
+ and $station->[2]{sched_dep} < $ret->{sched_dep_ts} )
+ )
{
$is_after = 1;
if ( @{$station} > 1 and not $dep_info ) {
@@ -235,13 +510,17 @@ sub postprocess {
$ret->{route_after} = \@route_after;
$ret->{extra_data} = $ret->{data};
$ret->{comment} = $ret->{user_data}{comment};
+ $ret->{wagongroups} = $ret->{user_data}{wagongroups};
+
+ $ret->{platform_type} = 'Gleis';
+ if ( $ret->{train_type} and $ret->{train_type} =~ m{ ast | bus | ruf }ix ) {
+ $ret->{platform_type} = 'Steig';
+ }
$ret->{visibility_str}
- = $ret->{visibility}
- ? $visibility_itoa{ $ret->{visibility} }
- : 'default';
+ = $visibility_itoa{ $ret->{visibility} // 'default' };
$ret->{effective_visibility_str}
- = $visibility_itoa{ $ret->{effective_visibility} };
+ = $visibility_itoa{ $ret->{effective_visibility} // 'default' };
my @parsed_messages;
for my $message ( @{ $ret->{messages} // [] } ) {
@@ -265,7 +544,7 @@ sub postprocess {
= $dep_info->{rt_arr}->epoch - $epoch;
}
- for my $station (@route_after) {
+ for my $station (@route) {
if ( @{$station} > 1 ) {
# Note: $station->[2]{sched_arr} may already have been
@@ -273,31 +552,25 @@ sub postprocess {
# station is present several times in a train's route, e.g.
# for Frankfurt Flughafen in some nightly connections.
my $times = $station->[2] // {};
- if ( $times->{sched_arr}
- and ref( $times->{sched_arr} ) ne 'DateTime' )
- {
- $times->{sched_arr}
- = epoch_to_dt( $times->{sched_arr} );
- if ( $times->{rt_arr} ) {
- $times->{rt_arr}
- = epoch_to_dt( $times->{rt_arr} );
- $times->{arr_delay}
- = $times->{rt_arr}->epoch - $times->{sched_arr}->epoch;
+ for my $key (qw(sched_arr rt_arr sched_dep rt_dep)) {
+ if ( $times->{$key} ) {
+ $times->{$key}
+ = epoch_or_dt_to_dt( $times->{$key} );
}
+ }
+ if ( $times->{sched_arr} and $times->{rt_arr} ) {
+ $times->{arr_delay}
+ = $times->{rt_arr}->epoch - $times->{sched_arr}->epoch;
+ }
+ if ( $times->{sched_arr} or $times->{rt_arr} ) {
$times->{arr} = $times->{rt_arr} || $times->{sched_arr};
$times->{arr_countdown} = $times->{arr}->epoch - $epoch;
}
- if ( $times->{sched_dep}
- and ref( $times->{sched_dep} ) ne 'DateTime' )
- {
- $times->{sched_dep}
- = epoch_to_dt( $times->{sched_dep} );
- if ( $times->{rt_dep} ) {
- $times->{rt_dep}
- = epoch_to_dt( $times->{rt_dep} );
- $times->{dep_delay}
- = $times->{rt_dep}->epoch - $times->{sched_dep}->epoch;
- }
+ if ( $times->{sched_dep} and $times->{rt_dep} ) {
+ $times->{dep_delay}
+ = $times->{rt_dep}->epoch - $times->{sched_dep}->epoch;
+ }
+ if ( $times->{sched_dep} or $times->{rt_dep} ) {
$times->{dep} = $times->{rt_dep} || $times->{sched_dep};
$times->{dep_countdown} = $times->{dep}->epoch - $epoch;
}
@@ -339,7 +612,7 @@ sub get {
my $table = 'in_transit';
- if ( $opt{with_timestamps} ) {
+ if ( $opt{with_timestamps} or $opt{with_polyline} ) {
$table = 'in_transit_str';
}
@@ -353,13 +626,16 @@ sub get {
$ret = $res->hash;
}
+ if ( $opt{with_polyline} and $ret ) {
+ $ret->{dep_latlon} = [ $ret->{dep_lat}, $ret->{dep_lon} ];
+ $ret->{arr_latlon} = [ $ret->{arr_lat}, $ret->{arr_lon} ];
+ }
+
if ( $opt{with_visibility} and $ret ) {
$ret->{visibility_str}
- = $ret->{visibility}
- ? $visibility_itoa{ $ret->{visibility} }
- : 'default';
+ = $visibility_itoa{ $ret->{visibility} // 'default' };
$ret->{effective_visibility_str}
- = $visibility_itoa{ $ret->{effective_visibility} };
+ = $visibility_itoa{ $ret->{effective_visibility} // 'default' };
}
if ( $opt{postprocess} and $ret ) {
@@ -408,17 +684,20 @@ sub get_all_active {
->hashes->each;
}
-sub get_checkout_station_id {
+sub get_checkout_ids {
my ( $self, %opt ) = @_;
my $uid = $opt{uid};
my $db = $opt{db} // $self->{pg}->db;
- my $status = $db->select( 'in_transit', ['checkout_station_id'],
- { user_id => $uid } )->hash;
+ my $status = $db->select(
+ 'in_transit',
+ [ 'checkout_station_id', 'backend_id' ],
+ { user_id => $uid }
+ )->hash;
if ($status) {
- return $status->{checkout_station_id};
+ return $status->{checkout_station_id}, $status->{backend_id};
}
return;
}
@@ -457,13 +736,6 @@ sub set_arrival {
my $uid = $opt{uid};
my $db = $opt{db} // $self->{pg}->db;
my $train = $opt{train};
- my $route = $opt{route};
-
- $route = $self->_merge_old_route(
- db => $db,
- uid => $uid,
- route => $route
- );
my $json = JSON->new;
@@ -474,7 +746,6 @@ sub set_arrival {
arr_platform => $train->platform,
sched_arrival => $train->sched_arrival,
real_arrival => $train->arrival,
- route => $json->encode($route),
messages => $json->encode(
[ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ]
)
@@ -566,7 +837,8 @@ sub set_polyline {
$self->set_polyline_id(
uid => $uid,
db => $db,
- polyline_id => $polyline_id
+ polyline_id => $polyline_id,
+ train_id => $opt{train_id},
);
}
@@ -579,11 +851,13 @@ sub set_polyline_id {
my $db = $opt{db} // $self->{pg}->db;
my $polyline_id = $opt{polyline_id};
- $db->update(
- 'in_transit',
- { polyline_id => $polyline_id },
- { user_id => $uid }
- );
+ my %where = ( user_id => $uid );
+
+ if ( $opt{train_id} ) {
+ $where{train_id} = $opt{train_id};
+ }
+
+ $db->update( 'in_transit', { polyline_id => $polyline_id }, \%where );
}
sub set_route_data {
@@ -596,6 +870,12 @@ sub set_route_data {
my $qos_msg = $opt{qos_messages};
my $him_msg = $opt{him_messages};
+ my %where = ( user_id => $uid );
+
+ if ( $opt{train_id} ) {
+ $where{train_id} = $opt{train_id};
+ }
+
my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } )
->expand->hash;
@@ -612,7 +892,7 @@ sub set_route_data {
route => JSON->new->encode($route),
data => JSON->new->encode($data)
},
- { user_id => $uid }
+ \%where
);
}
@@ -699,6 +979,128 @@ sub update_departure_cancelled {
return $rows;
}
+sub update_departure_dbris {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stop = $opt{stop};
+ my $json = JSON->new;
+
+ my $res_h = $db->select( 'in_transit', [ 'data', 'user_data' ],
+ { user_id => $uid } )->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ my $persistent_data = $res_h ? $res_h->{user_data} : {};
+
+ if ( $stop->{rt_dep} ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
+ $ephemeral_data->{him_msg} = [];
+ $persistent_data->{him_msg} = [];
+ for my $msg ( $journey->messages ) {
+ if ( not $msg->{ueberschrift} ) {
+ push(
+ @{ $ephemeral_data->{him_msg} },
+ {
+ header => q{},
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ push(
+ @{ $persistent_data->{him_msg} },
+ {
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ }
+ }
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_departure => $stop->{rt_dep},
+ data => $json->encode($ephemeral_data),
+ user_data => $json->encode($persistent_data),
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{train_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
+sub update_departure_efa {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stop = $opt{stop};
+ my $json = JSON->new;
+
+ my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } )
+ ->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ if ( $stop->rt_dep ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ data => $json->encode($ephemeral_data),
+ real_departure => $stop->rt_dep,
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{trip_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
+sub update_departure_motis {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stopover = $opt{stopover};
+ my $json = JSON->new;
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_departure => $stopover->{realtime_departure},
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{train_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
sub update_departure_hafas {
my ( $self, %opt ) = @_;
my $uid = $opt{uid};
@@ -709,12 +1111,20 @@ sub update_departure_hafas {
my $stop = $opt{stop};
my $json = JSON->new;
+ my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } )
+ ->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ if ( $stop->{rt_dep} ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
# selecting on user_id and train_no avoids a race condition if a user checks
# into a new train while we are fetching data for their previous journey. In
# this case, the new train would receive data from the previous journey.
$db->update(
'in_transit',
{
+ data => $json->encode($ephemeral_data),
real_departure => $stop->{rt_dep},
},
{
@@ -768,6 +1178,210 @@ sub update_arrival {
return $rows;
}
+sub update_arrival_dbris {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stop = $opt{stop};
+ my $json = JSON->new;
+
+ my $res_h = $db->select( 'in_transit', [ 'data', 'user_data' ],
+ { user_id => $uid } )->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ my $persistent_data = $res_h ? $res_h->{user_data} : {};
+
+ if ( $stop->{rt_arr} ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
+ $ephemeral_data->{him_msg} = [];
+ $persistent_data->{him_msg} = [];
+ for my $msg ( $journey->messages ) {
+ if ( not $msg->{ueberschrift} ) {
+ push(
+ @{ $ephemeral_data->{him_msg} },
+ {
+ header => q{},
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ push(
+ @{ $persistent_data->{him_msg} },
+ {
+ prio => $msg->{prioritaet},
+ lead => $msg->{text}
+ }
+ );
+ }
+ }
+
+ my @route;
+ for my $j_stop ( $journey->route ) {
+ push(
+ @route,
+ [
+ $j_stop->name,
+ $j_stop->eva,
+ {
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ platform => $j_stop->platform,
+ isCancelled => $j_stop->is_cancelled,
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ load => {
+ FIRST => $j_stop->occupancy_first,
+ SECOND => $j_stop->occupancy_second
+ },
+ lat => $j_stop->lat,
+ lon => $j_stop->lon,
+ }
+ ]
+ );
+ }
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_arrival => $stop->rt_arr,
+ arr_platform => $stop->platform,
+ route => $json->encode( [@route] ),
+ data => $json->encode($ephemeral_data),
+ user_data => $json->encode($persistent_data),
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{train_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
+sub update_arrival_efa {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stop = $opt{stop};
+ my $json = JSON->new;
+
+ my $res_h
+ = $db->select( 'in_transit', [ 'data', 'route' ], { user_id => $uid } )
+ ->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ my $old_route = $res_h ? $res_h->{route} : [];
+
+ if ( $stop->rt_arr ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
+ my @route;
+ for my $j_stop ( $journey->route ) {
+ push(
+ @route,
+ [
+ $j_stop->full_name,
+ $j_stop->id_num,
+ {
+ sched_arr => _epoch( $j_stop->sched_arr ),
+ sched_dep => _epoch( $j_stop->sched_dep ),
+ rt_arr => _epoch( $j_stop->rt_arr ),
+ rt_dep => _epoch( $j_stop->rt_dep ),
+ isCancelled => $j_stop->is_cancelled,
+ arr_delay => $j_stop->arr_delay,
+ dep_delay => $j_stop->dep_delay,
+ efa_load => $j_stop->occupancy,
+ lat => $j_stop->latlon->[0],
+ lon => $j_stop->latlon->[1],
+ }
+ ]
+ );
+ }
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ data => $json->encode($ephemeral_data),
+ real_arrival => $stop->rt_arr,
+ arr_platform => $stop->platform,
+ route => $json->encode( [@route] ),
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{trip_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
+sub update_arrival_motis {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+ my $dep_eva = $opt{dep_eva};
+ my $arr_eva = $opt{arr_eva};
+ my $journey = $opt{journey};
+ my $stopover = $opt{stopover};
+ my $json = JSON->new;
+
+ my @route;
+ for my $journey_stopover ( $journey->stopovers ) {
+ push(
+ @route,
+ [
+ $journey_stopover->stop->name,
+ $journey_stopover->stop->{eva}
+ // die('eva not set for stopover'),
+ {
+ sched_arr => _epoch( $journey_stopover->scheduled_arrival ),
+ sched_dep =>
+ _epoch( $journey_stopover->scheduled_departure ),
+ rt_arr => _epoch( $journey_stopover->realtime_arrival ),
+ rt_dep => _epoch( $journey_stopover->realtime_departure ),
+ arr_delay => $journey_stopover->arrival_delay,
+ dep_delay => $journey_stopover->departure_delay,
+ lat => $journey_stopover->stop->lat,
+ lon => $journey_stopover->stop->lon,
+ }
+ ]
+ );
+ }
+
+ # selecting on user_id and train_no avoids a race condition if a user checks
+ # into a new train while we are fetching data for their previous journey. In
+ # this case, the new train would receive data from the previous journey.
+ $db->update(
+ 'in_transit',
+ {
+ real_arrival => $stopover->realtime_arrival,
+ arr_platform => $stopover->track,
+ route => $json->encode( [@route] ),
+ },
+ {
+ user_id => $uid,
+ train_id => $opt{train_id},
+ checkin_station_id => $dep_eva,
+ checkout_station_id => $arr_eva,
+ }
+ );
+}
+
sub update_arrival_hafas {
my ( $self, %opt ) = @_;
my $uid = $opt{uid};
@@ -778,7 +1392,16 @@ sub update_arrival_hafas {
my $stop = $opt{stop};
my $json = JSON->new;
- # TODO use old rt data if available
+ my $res_h
+ = $db->select( 'in_transit', [ 'data', 'route' ], { user_id => $uid } )
+ ->expand->hash;
+ my $ephemeral_data = $res_h ? $res_h->{data} : {};
+ my $old_route = $res_h ? $res_h->{route} : [];
+
+ if ( $stop->{rt_arr} ) {
+ $ephemeral_data->{rt} = 1;
+ }
+
my @route;
for my $j_stop ( $journey->route ) {
push(
@@ -793,7 +1416,9 @@ sub update_arrival_hafas {
rt_dep => _epoch( $j_stop->rt_dep ),
arr_delay => $j_stop->arr_delay,
dep_delay => $j_stop->dep_delay,
- load => $j_stop->load
+ load => $j_stop->load,
+ lat => $j_stop->loc->lat,
+ lon => $j_stop->loc->lon,
}
]
);
@@ -802,10 +1427,6 @@ sub update_arrival_hafas {
}
}
- my $res_h = $db->select( 'in_transit', ['route'], { user_id => $uid } )
- ->expand->hash;
- my $old_route = $res_h ? $res_h->{route} : [];
-
for my $i ( 0 .. $#route ) {
if ( $old_route->[$i] and $old_route->[$i][1] == $route[$i][1] ) {
for my $k (qw(rt_arr rt_dep arr_delay dep_delay)) {
@@ -820,7 +1441,9 @@ sub update_arrival_hafas {
$db->update(
'in_transit',
{
- real_arrival => $stop->{rt_arr},
+ data => $json->encode($ephemeral_data),
+ real_arrival => $stop->rt_arr,
+ arr_platform => $stop->platform,
route => $json->encode( [@route] ),
},
{
@@ -839,6 +1462,12 @@ sub update_data {
my $db = $opt{db} // $self->{pg}->db;
my $new_data = $opt{data} // {};
+ my %where = ( user_id => $uid );
+
+ if ( $opt{train_id} ) {
+ $where{train_id} = $opt{train_id};
+ }
+
my $res_h = $db->select( 'in_transit', ['data'], { user_id => $uid } )
->expand->hash;
@@ -848,11 +1477,7 @@ sub update_data {
$data->{$k} = $v;
}
- $db->update(
- 'in_transit',
- { data => JSON->new->encode($data) },
- { user_id => $uid }
- );
+ $db->update( 'in_transit', { data => JSON->new->encode($data) }, \%where );
}
sub update_user_data {
@@ -862,6 +1487,12 @@ sub update_user_data {
my $db = $opt{db} // $self->{pg}->db;
my $new_data = $opt{user_data} // {};
+ my %where = ( user_id => $uid );
+
+ if ( $opt{train_id} ) {
+ $where{train_id} = $opt{train_id};
+ }
+
my $res_h = $db->select( 'in_transit', ['user_data'], { user_id => $uid } )
->expand->hash;
@@ -871,11 +1502,8 @@ sub update_user_data {
$data->{$k} = $v;
}
- $db->update(
- 'in_transit',
- { user_data => JSON->new->encode($data) },
- { user_id => $uid }
- );
+ $db->update( 'in_transit',
+ { user_data => JSON->new->encode($data) }, \%where );
}
sub update_visibility {
diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm
index 97c4681..b07511a 100755
--- a/lib/Travelynx/Model/Journeys.pm
+++ b/lib/Travelynx/Model/Journeys.pm
@@ -4,16 +4,16 @@ package Travelynx::Model::Journeys;
#
# SPDX-License-Identifier: AGPL-3.0-or-later
-use GIS::Distance;
-use List::MoreUtils qw(after_incl before_incl);
-
use strict;
use warnings;
use 5.020;
use utf8;
use DateTime;
+use DateTime::Format::Strptime;
+use GIS::Distance;
use JSON;
+use List::MoreUtils qw(after_incl before_incl);
my %visibility_itoa = (
100 => 'public',
@@ -118,8 +118,10 @@ sub add {
my $db = $opt{db};
my $uid = $opt{uid};
my $now = DateTime->now( time_zone => 'Europe/Berlin' );
- my $dep_station = $self->{stations}->search( $opt{dep_station} );
- my $arr_station = $self->{stations}->search( $opt{arr_station} );
+ my $dep_station = $self->{stations}
+ ->search( $opt{dep_station}, backend_id => $opt{backend_id} );
+ my $arr_station = $self->{stations}
+ ->search( $opt{arr_station}, backend_id => $opt{backend_id} );
if ( not $dep_station ) {
return ( undef, 'Unbekannter Startbahnhof' );
@@ -167,16 +169,60 @@ sub add {
my @route;
if ( not $route_has_start ) {
- push( @route, [ $dep_station->{name}, $dep_station->{eva}, {} ] );
+ push(
+ @route,
+ [
+ $dep_station->{name},
+ $dep_station->{eva},
+ {
+ lat => $dep_station->{lat},
+ lon => $dep_station->{lon},
+ }
+ ]
+ );
}
if ( $opt{route} ) {
+ my $parser = DateTime::Format::Strptime->new(
+ pattern => '%d.%m.%Y %H:%M',
+ locale => 'de_DE',
+ time_zone => 'Europe/Berlin'
+ );
my @unknown_stations;
+ my $prev_epoch = 0;
+
for my $station ( @{ $opt{route} } ) {
- my $station_info = $self->{stations}->search($station);
+ my $ts;
+ my %station_data;
+ if ( $station
+ =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x )
+ {
+ $station = $+{stop};
+ $ts = $parser->parse_datetime( $+{timestamp} );
+ if ($ts) {
+ my $epoch = $ts->epoch;
+ if ( $epoch < $prev_epoch ) {
+ return ( undef,
+'Zeitstempel der Unterwegshalte müssen monoton steigend sein (keine Zeitreisen und keine Portale)'
+ );
+ }
+ $station_data{sched_arr} = $epoch;
+ $station_data{sched_dep} = $epoch;
+ $prev_epoch = $epoch;
+ }
+ }
+ my $station_info = $self->{stations}
+ ->search( $station, backend_id => $opt{backend_id} );
if ($station_info) {
- push( @route,
- [ $station_info->{name}, $station_info->{eva}, {} ] );
+ $station_data{lat} = $station_info->{lat};
+ $station_data{lon} = $station_info->{lon};
+ push(
+ @route,
+ [
+ $station_info->{name}, $station_info->{eva},
+ \%station_data,
+ ]
+ );
}
else {
push( @route, [ $station, undef, {} ] );
@@ -198,7 +244,17 @@ sub add {
}
if ( not $route_has_stop ) {
- push( @route, [ $arr_station->{name}, $arr_station->{eva}, {} ] );
+ push(
+ @route,
+ [
+ $arr_station->{name},
+ $arr_station->{eva},
+ {
+ lat => $arr_station->{lat},
+ lon => $arr_station->{lon},
+ }
+ ]
+ );
}
my $entry = {
@@ -218,6 +274,7 @@ sub add {
edited => 0x3fff,
cancelled => $opt{cancelled} ? 1 : 0,
route => JSON->new->encode( \@route ),
+ backend_id => $opt{backend_id},
};
if ( $opt{comment} ) {
@@ -250,8 +307,14 @@ sub add_from_in_transit {
my $db = $opt{db};
my $journey = $opt{journey};
+ if ( $journey->{train_id} eq 'manual' ) {
+ $journey->{edited} = 0x3fff;
+ }
+ else {
+ $journey->{edited} = 0;
+ }
+
delete $journey->{data};
- $journey->{edited} = 0;
$journey->{checkout_time} = DateTime->now( time_zone => 'Europe/Berlin' );
return $db->insert( 'journeys', $journey, { returning => 'id' } )
@@ -268,10 +331,11 @@ sub update {
my $rows;
my $journey = $self->get_single(
- uid => $uid,
- db => $db,
- journey_id => $journey_id,
- with_datetime => 1,
+ uid => $uid,
+ db => $db,
+ journey_id => $journey_id,
+ with_datetime => 1,
+ with_route_datetime => 1,
);
eval {
@@ -515,7 +579,7 @@ sub get {
my @select
= (
- qw(journey_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility)
+ qw(journey_id is_dbris is_iris is_hafas is_motis backend_name backend_id train_type train_line train_no checkin_ts sched_dep_ts real_dep_ts dep_eva dep_ds100 dep_name dep_platform dep_lat dep_lon checkout_ts sched_arr_ts real_arr_ts arr_eva arr_ds100 arr_name arr_platform arr_lat arr_lon cancelled edited route messages user_data visibility effective_visibility)
);
my %where = (
user_id => $uid,
@@ -573,12 +637,19 @@ sub get {
my $ref = {
id => $entry->{journey_id},
- type => $entry->{train_type},
+ is_dbris => $entry->{is_dbris},
+ is_iris => $entry->{is_iris},
+ is_hafas => $entry->{is_hafas},
+ is_motis => $entry->{is_motis},
+ backend_name => $entry->{backend_name},
+ backend_id => $entry->{backend_id},
+ type => $entry->{train_type} =~ s{ \s+ $ }{}rx,
line => $entry->{train_line},
no => $entry->{train_no},
from_eva => $entry->{dep_eva},
from_ds100 => $entry->{dep_ds100},
from_name => $entry->{dep_name},
+ from_platform => $entry->{dep_platform},
from_latlon => [ $entry->{dep_lat}, $entry->{dep_lon} ],
checkin_ts => $entry->{checkin_ts},
sched_dep_ts => $entry->{sched_dep_ts},
@@ -586,6 +657,7 @@ sub get {
to_eva => $entry->{arr_eva},
to_ds100 => $entry->{arr_ds100},
to_name => $entry->{arr_name},
+ to_platform => $entry->{arr_platform},
to_latlon => [ $entry->{arr_lat}, $entry->{arr_lon} ],
checkout_ts => $entry->{checkout_ts},
sched_arr_ts => $entry->{sched_arr_ts},
@@ -619,6 +691,14 @@ sub get {
$ref->{checkout} = epoch_to_dt( $ref->{checkout_ts} );
$ref->{sched_arrival} = epoch_to_dt( $ref->{sched_arr_ts} );
$ref->{rt_arrival} = epoch_to_dt( $ref->{rt_arr_ts} );
+ if ( $ref->{rt_dep_ts} and $ref->{sched_dep_ts} ) {
+ $ref->{delay_dep} = $ref->{rt_dep_ts} - $ref->{sched_dep_ts};
+ }
+ if ( $ref->{rt_arr_ts} and $ref->{sched_arr_ts} ) {
+ $ref->{delay_arr} = $ref->{rt_arr_ts} - $ref->{sched_arr_ts};
+ }
+ }
+ if ( $opt{with_route_datetime} ) {
for my $stop ( @{ $ref->{route} } ) {
for my $k (qw(rt_arr rt_dep sched_arr sched_dep)) {
if ( $stop->[2]{$k} ) {
@@ -632,7 +712,10 @@ sub get {
my $rename = $self->{renamed_station};
for my $stop ( @{ $ref->{route} } ) {
if ( $stop->[0] =~ m{^Betriebsstelle nicht bekannt (\d+)$} ) {
- if ( my $s = $self->{stations}->get_by_eva($1) ) {
+ if ( my $s
+ = $self->{stations}
+ ->get_by_eva( $1, backend_id => $ref->{backend_id} ) )
+ {
$stop->[0] = $s->{name};
}
}
@@ -767,14 +850,40 @@ sub get_oldest_ts {
return undef;
}
-sub get_latest_checkout_station_id {
+sub get_latest_checkout_latlon {
+ my ( $self, %opt ) = @_;
+ my $uid = $opt{uid};
+ my $db = $opt{db} // $self->{pg}->db;
+
+ my $res_h = $db->select(
+ 'journeys_str',
+ [ 'arr_lat', 'arr_lon', ],
+ {
+ user_id => $uid,
+ cancelled => 0
+ },
+ {
+ limit => 1,
+ order_by => { -desc => 'journey_id' }
+ }
+ )->hash;
+
+ if ( not $res_h ) {
+ return;
+ }
+
+ return $res_h->{arr_lat}, $res_h->{arr_lon};
+
+}
+
+sub get_latest_checkout_ids {
my ( $self, %opt ) = @_;
my $uid = $opt{uid};
my $db = $opt{db} // $self->{pg}->db;
my $res_h = $db->select(
'journeys',
- ['checkout_station_id'],
+ [ 'checkout_station_id', 'backend_id', ],
{
user_id => $uid,
cancelled => 0
@@ -789,7 +898,7 @@ sub get_latest_checkout_station_id {
return;
}
- return $res_h->{checkout_station_id};
+ return $res_h->{checkout_station_id}, $res_h->{backend_id};
}
sub get_latest_checkout_stations {
@@ -800,7 +909,13 @@ sub get_latest_checkout_stations {
my $res = $db->select(
'journeys_str',
- [ 'arr_name', 'arr_eva', 'train_id' ],
+ [
+ 'arr_name', 'arr_eva',
+ 'arr_external_id', 'train_id',
+ 'backend_id', 'backend_name',
+ 'is_dbris', 'is_efa',
+ 'is_hafas', 'is_motis'
+ ],
{
user_id => $uid,
cancelled => 0
@@ -821,9 +936,15 @@ sub get_latest_checkout_stations {
push(
@ret,
{
- name => $row->{arr_name},
- eva => $row->{arr_eva},
- hafas => ( $row->{train_id} =~ m{[|]} ? 1 : 0 ),
+ name => $row->{arr_name},
+ eva => $row->{arr_eva},
+ external_id_or_eva => $row->{arr_external_id}
+ // $row->{arr_eva},
+ dbris => $row->{is_dbris} ? $row->{backend_name} : 0,
+ efa => $row->{is_efa} ? $row->{backend_name} : 0,
+ hafas => $row->{is_hafas} ? $row->{backend_name} : 0,
+ motis => $row->{is_motis} ? $row->{backend_name} : 0,
+ backend_id => $row->{backend_id},
}
);
}
@@ -1021,32 +1142,33 @@ sub sanity_check {
if ( defined $journey->{sched_duration}
and $journey->{sched_duration} <= 0 )
{
- return
-'Die geplante Dauer dieser Fahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.';
+ return 'Die geplante Dauer dieser Fahrt ist ≤ 0.'
+ . ' Teleportation und Zeitreisen werden in diesem Universum nicht unterstützt.';
}
if ( defined $journey->{rt_duration}
and $journey->{rt_duration} <= 0 )
{
- return
-'Die Dauer dieser Fahrt ist ≤ 0. Teleportation und Zeitreisen werden aktuell nicht unterstützt.';
+ return 'Die Dauer dieser Fahrt ist ≤ 0.'
+ . ' Teleportation und Zeitreisen werden in diesem Universum nicht unterstützt.';
}
if ( $journey->{sched_duration}
- and $journey->{sched_duration} > 60 * 60 * 24 )
+ and $journey->{sched_duration} > 60 * 60 * 72 )
{
- return 'Die Fahrt ist länger als 24 Stunden.';
+ return 'Die Fahrt ist länger als drei Tage.';
}
if ( $journey->{rt_duration}
- and $journey->{rt_duration} > 60 * 60 * 24 )
+ and $journey->{rt_duration} > 60 * 60 * 72 )
{
- return 'Die Fahrt ist länger als 24 Stunden.';
+ return 'Die Fahrt ist länger als drei Tage.';
}
if ( $journey->{kmh_route} > 500 or $journey->{kmh_beeline} > 500 ) {
- return 'Fahrten mit über 500 km/h? Schön wär\'s.';
+ return 'Die berechnete Geschwindigkeit beträgt über 500 km/h.'
+ . ' Das wirkt unrealistisch.';
}
if ( $journey->{route} and @{ $journey->{route} } > 199 ) {
my $stop_count = @{ $journey->{route} };
- return
-"Die Fahrt hat $stop_count Unterwegshalte. Also ich weiß ja nicht so recht.";
+ return "Die Fahrt hat $stop_count Unterwegshalte. "
+ . ' Stimmt das wirklich?';
}
if ( $journey->{edited} & 0x0010 and not $lax ) {
my @unknown_stations
@@ -1082,19 +1204,62 @@ sub get_travel_distance {
->warn("Journey $journey->{id} has no from_name for EVA $from_eva");
}
+ # Work around inconsistencies caused by a multiple EVA IDs mapping to the same station name
+ if (
+ @{ $polyline_ref // [] }
+ and not List::MoreUtils::any { $_->[2] and $_->[2] == $from_eva }
+ @{ $polyline_ref // [] }
+ )
+ {
+ $self->{log}->debug(
+"Journey $journey->{id} from_eva ($from_eva) is not part of polyline"
+ );
+ for my $entry ( @{$route_ref} ) {
+ if ( $entry->[0] eq $from and $entry->[1] ) {
+ $from_eva = $entry->[1];
+ $self->{log}->debug("... setting to $from_eva");
+ last;
+ }
+ }
+ }
+ if (
+ @{ $polyline_ref // [] }
+ and not List::MoreUtils::any { $_->[2] and $_->[2] == $to_eva }
+ @{ $polyline_ref // [] }
+ )
+ {
+ $self->{log}->debug(
+ "Journey $journey->{id} to_eva ($to_eva) is not part of polyline");
+ for my $entry ( @{$route_ref} ) {
+ if ( $entry->[0] eq $to and $entry->[1] ) {
+ $to_eva = $entry->[1];
+ $self->{log}->debug("... setting to $to_eva");
+ last;
+ }
+ }
+ }
+
my $distance_polyline = 0;
my $distance_intermediate = 0;
- my $distance_beeline = 0;
- my $skipped = 0;
my $geo = GIS::Distance->new();
- my @stations = map { $_->[0] } @{$route_ref};
- my @route = after_incl { $_ eq $from } @stations;
- @route = before_incl { $_ eq $to } @route;
+ my $distance_beeline
+ = $geo->distance_metal( @{$from_latlon}, @{$to_latlon} );
+ my @route
+ = after_incl { ( $_->[1] and $_->[1] == $from_eva ) or $_->[0] eq $from }
+ @{$route_ref};
+ @route
+ = before_incl { ( $_->[1] and $_->[1] == $to_eva ) or $_->[0] eq $to }
+ @route;
- if ( @route < 2 ) {
+ if (
+ @route < 2
+ or ( $route[-1][0] ne $to
+ and ( not $route[-1][1] or $route[-1][1] != $to_eva ) )
+ )
+ {
# I AM ERROR
- return ( 0, 0, 0 );
+ return ( 0, 0, $distance_beeline );
}
my @polyline = after_incl { $_->[2] and $_->[2] == $from_eva }
@@ -1102,34 +1267,32 @@ sub get_travel_distance {
@polyline
= before_incl { $_->[2] and $_->[2] == $to_eva } @polyline;
- my $prev_station = shift @polyline;
- for my $station (@polyline) {
- $distance_polyline += $geo->distance_metal(
- $prev_station->[1], $prev_station->[0],
- $station->[1], $station->[0]
- );
- $prev_station = $station;
- }
-
- $prev_station = $self->{latlon_by_station}->{ shift @route };
- if ( not $prev_station ) {
- return ( $distance_polyline, 0, 0 );
- }
-
- for my $station_name (@route) {
- if ( my $station = $self->{latlon_by_station}->{$station_name} ) {
- $distance_intermediate += $geo->distance_metal(
- $prev_station->[0], $prev_station->[1],
- $station->[0], $station->[1]
+ # ensure that before_incl matched -- otherwise, @polyline is too long
+ if ( @polyline and $polyline[-1][2] == $to_eva ) {
+ my $prev_station = shift @polyline;
+ for my $station (@polyline) {
+ $distance_polyline += $geo->distance_metal(
+ $prev_station->[1], $prev_station->[0],
+ $station->[1], $station->[0]
);
$prev_station = $station;
}
}
- $distance_beeline = $geo->distance_metal( @{$from_latlon}, @{$to_latlon} );
+ if ( defined $route[0][2]{lat} and defined $route[0][2]{lon} ) {
+ my $prev_station = shift @route;
+ for my $station (@route) {
+ if ( defined $station->[2]{lat} and defined $station->[2]{lon} ) {
+ $distance_intermediate += $geo->distance_metal(
+ $prev_station->[2]{lat}, $prev_station->[2]{lon},
+ $station->[2]{lat}, $station->[2]{lon}
+ );
+ $prev_station = $station;
+ }
+ }
+ }
- return ( $distance_polyline, $distance_intermediate,
- $distance_beeline, $skipped );
+ return ( $distance_polyline, $distance_intermediate, $distance_beeline );
}
sub grep_single {
@@ -1548,7 +1711,10 @@ sub compute_stats {
@inconsistencies,
{
conflict => {
- train => $journey->{type} . ' '
+ train => (
+ $journey->{is_motis} ? '' : $journey->{type}
+ )
+ . ' '
. ( $journey->{line} // $journey->{no} ),
arr => epoch_to_dt( $journey->{rt_arr_ts} )
->strftime('%d.%m.%Y %H:%M'),
@@ -1574,7 +1740,8 @@ sub compute_stats {
$next_departure = $journey->{rt_dep_ts};
$next_id = $journey->{id};
$next_train
- = $journey->{type} . ' ' . ( $journey->{line} // $journey->{no} ),;
+ = ( $journey->{is_motis} ? '' : $journey->{type} ) . ' '
+ . ( $journey->{line} // $journey->{no} ),;
}
my $ret = {
km_route => $km_route,
@@ -1607,6 +1774,8 @@ sub compute_stats {
sub get_stats {
my ( $self, %opt ) = @_;
+ $self->{log}->debug("get_stats");
+
if ( $opt{cancelled} ) {
$self->{log}
->warn('get_journey_stats called with illegal option cancelled => 1');
@@ -1633,9 +1802,12 @@ sub get_stats {
)
)
{
+ $self->{log}->debug("got cached journey stats for $year/$month");
return $stats;
}
+ $self->{log}->debug("computing journey stats for $year/$month");
+
my $interval_start = DateTime->new(
time_zone => 'Europe/Berlin',
year => 2000,
@@ -1694,28 +1866,29 @@ sub get_stats {
return $stats;
}
-sub get_latest_dest_id {
+sub get_latest_dest_ids {
my ( $self, %opt ) = @_;
my $uid = $opt{uid};
my $db = $opt{db} // $self->{pg}->db;
if (
- my $id = $self->{in_transit}->get_checkout_station_id(
+ my ( $id, $backend_id ) = $self->{in_transit}->get_checkout_ids(
uid => $uid,
db => $db
)
)
{
- return $id;
+ return ( $id, $backend_id );
}
- return $self->get_latest_checkout_station_id(
+ return $self->get_latest_checkout_ids(
uid => $uid,
db => $db
);
}
+# Returns a listref of {eva, name} hashrefs for the specified backend.
sub get_connection_targets {
my ( $self, %opt ) = @_;
@@ -1724,21 +1897,32 @@ sub get_connection_targets {
// DateTime->now( time_zone => 'Europe/Berlin' )->subtract( months => 4 );
my $db = $opt{db} //= $self->{pg}->db;
my $min_count = $opt{min_count} // 3;
+ my $dest_id = $opt{eva};
if ( $opt{destination_name} ) {
- return (
- [],
- [ { eva => $opt{eva}, name => $opt{destination_name} } ]
- );
+ return {
+ eva => $opt{eva},
+ name => $opt{destination_name}
+ };
}
- my $dest_id = $opt{eva} // $self->get_latest_dest_id(%opt);
+ my $backend_id = $opt{backend_id};
if ( not $dest_id ) {
- return ( [], [] );
+ ( $dest_id, $backend_id ) = $self->get_latest_dest_ids(%opt);
}
- my $dest_ids = [ $dest_id, $self->{stations}->get_meta( eva => $dest_id ) ];
+ if ( not $dest_id ) {
+ return;
+ }
+
+ my $dest_ids = [
+ $dest_id,
+ $self->{stations}->get_meta(
+ eva => $dest_id,
+ backend_id => $backend_id,
+ )
+ ];
my $res = $db->select(
'journeys',
@@ -1746,7 +1930,8 @@ sub get_connection_targets {
{
user_id => $uid,
checkin_station_id => $dest_ids,
- real_departure => { '>', $threshold }
+ real_departure => { '>', $threshold },
+ backend_id => $opt{backend_id},
},
{
group_by => ['checkout_station_id'],
@@ -1755,9 +1940,13 @@ sub get_connection_targets {
);
my @destinations
= $res->hashes->grep( sub { shift->{count} >= $min_count } )
- ->map( sub { shift->{dest} } )->each;
- @destinations = $self->{stations}->get_by_evas(@destinations);
- return ( $dest_ids, \@destinations );
+ ->map( sub { shift->{dest} } )
+ ->each;
+ @destinations = $self->{stations}->get_by_evas(
+ backend_id => $opt{backend_id},
+ evas => [@destinations]
+ );
+ return @destinations;
}
sub update_visibility {
diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm
index ac4019c..c6d9730 100644
--- a/lib/Travelynx/Model/Stations.pm
+++ b/lib/Travelynx/Model/Stations.pm
@@ -1,6 +1,7 @@
package Travelynx::Model::Stations;
# Copyright (C) 2022 Birte Kristina Friesel
+# Copyright (C) 2025 networkException <git@nwex.de>
#
# SPDX-License-Identifier: AGPL-3.0-or-later
@@ -14,56 +15,319 @@ sub new {
return bless( \%opt, $class );
}
+sub get_backend_id {
+ my ( $self, %opt ) = @_;
+
+ if ( $opt{iris} ) {
+
+ # special case
+ return 0;
+ }
+ if ( $opt{dbris} and $self->{backend_id}{dbris}{ $opt{dbris} } ) {
+ return $self->{backend_id}{dbris}{ $opt{dbris} };
+ }
+ if ( $opt{efa} and $self->{backend_id}{efa}{ $opt{efa} } ) {
+ return $self->{backend_id}{efa}{ $opt{efa} };
+ }
+ if ( $opt{hafas} and $self->{backend_id}{hafas}{ $opt{hafas} } ) {
+ return $self->{backend_id}{hafas}{ $opt{hafas} };
+ }
+ if ( $opt{motis} and $self->{backend_id}{motis}{ $opt{motis} } ) {
+ return $self->{backend_id}{motis}{ $opt{motis} };
+ }
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $backend_id = 0;
+
+ if ( $opt{dbris} ) {
+ $backend_id = $db->select(
+ 'backends',
+ ['id'],
+ {
+ dbris => 1,
+ name => $opt{dbris}
+ }
+ )->hash->{id};
+ $self->{backend_id}{dbris}{ $opt{dbris} } = $backend_id;
+ }
+ elsif ( $opt{efa} ) {
+ $backend_id = $db->select(
+ 'backends',
+ ['id'],
+ {
+ efa => 1,
+ name => $opt{efa}
+ }
+ )->hash->{id};
+ $self->{backend_id}{efa}{ $opt{efa} } = $backend_id;
+ }
+ elsif ( $opt{hafas} ) {
+ $backend_id = $db->select(
+ 'backends',
+ ['id'],
+ {
+ hafas => 1,
+ name => $opt{hafas}
+ }
+ )->hash->{id};
+ $self->{backend_id}{hafas}{ $opt{hafas} } = $backend_id;
+ }
+ elsif ( $opt{motis} ) {
+ $backend_id = $db->select(
+ 'backends',
+ ['id'],
+ {
+ motis => 1,
+ name => $opt{motis}
+ }
+ )->hash->{id};
+ $self->{backend_id}{motis}{ $opt{motis} } = $backend_id;
+ }
+
+ return $backend_id;
+}
+
+sub get_backend {
+ my ( $self, %opt ) = @_;
+
+ if ( $self->{backend_cache}{ $opt{backend_id} } ) {
+ return $self->{backend_cache}{ $opt{backend_id} };
+ }
+
+ my $db = $opt{db} // $self->{pg}->db;
+ my $ret = $db->select(
+ 'backends',
+ '*',
+ {
+ id => $opt{backend_id},
+ }
+ )->hash;
+
+ $self->{backend_cache}{ $opt{backend_id} } = $ret;
+
+ return $ret;
+}
+
+sub get_backends {
+ my ( $self, %opt ) = @_;
+
+ $opt{db} //= $self->{pg}->db;
+
+ my $res = $opt{db}->select( 'backends',
+ [ 'id', 'name', 'dbris', 'efa', 'hafas', 'iris', 'motis' ] );
+ my @ret;
+
+ while ( my $row = $res->hash ) {
+ push(
+ @ret,
+ {
+ id => $row->{id},
+ name => $row->{name},
+ dbris => $row->{dbris},
+ efa => $row->{efa},
+ hafas => $row->{hafas},
+ iris => $row->{iris},
+ motis => $row->{motis},
+ }
+ );
+ }
+
+ return @ret;
+}
+
+# Slow for MOTIS backends
sub add_or_update {
my ( $self, %opt ) = @_;
- my $stop = $opt{stop};
- my $loc = $stop->loc;
- my $source = 1;
- my $db = $opt{db} // $self->{pg}->db;
+ my $stop = $opt{stop};
+ $opt{db} //= $self->{pg}->db;
+
+ $opt{backend_id} //= $self->get_backend_id(%opt);
+
+ if ( $opt{dbris} ) {
+ if (
+ my $s = $self->get_by_eva(
+ $stop->eva,
+ db => $opt{db},
+ backend_id => $opt{backend_id}
+ )
+ )
+ {
+ $opt{db}->update(
+ 'stations',
+ {
+ name => $stop->name,
+ lat => $stop->lat,
+ lon => $stop->lon,
+ archived => 0
+ },
+ {
+ eva => $stop->eva,
+ source => $opt{backend_id}
+ }
+ );
+ return;
+ }
+ $opt{db}->insert(
+ 'stations',
+ {
+ eva => $stop->eva,
+ name => $stop->name,
+ lat => $stop->lat,
+ lon => $stop->lon,
+ source => $opt{backend_id},
+ archived => 0
+ }
+ );
+ return;
+ }
+
+ if ( $opt{efa} ) {
+ if (
+ my $s = $self->get_by_eva(
+ $stop->id_num,
+ db => $opt{db},
+ backend_id => $opt{backend_id}
+ )
+ )
+ {
+ $opt{db}->update(
+ 'stations',
+ {
+ name => $stop->full_name,
+ lat => $stop->latlon->[0],
+ lon => $stop->latlon->[1],
+ archived => 0
+ },
+ {
+ eva => $stop->id_num,
+ source => $opt{backend_id}
+ }
+ );
+ return;
+ }
+ if (not $stop->latlon) {
+ die('Backend Error: Stop "' . $stop->full_name . '" has no geo coordinates');
+ }
+ $opt{db}->insert(
+ 'stations',
+ {
+ eva => $stop->id_num,
+ name => $stop->full_name,
+ lat => $stop->latlon->[0],
+ lon => $stop->latlon->[1],
+ source => $opt{backend_id},
+ archived => 0
+ }
+ );
+ return;
+ }
+
+ if ( $opt{motis} ) {
+ if (
+ my $s = $self->get_by_external_id(
+ external_id => $stop->id,
+ db => $opt{db},
+ backend_id => $opt{backend_id}
+ )
+ )
+ {
+ $opt{db}->update(
+ 'stations',
+ {
+ name => $stop->name,
+ lat => $stop->lat,
+ lon => $stop->lon,
+ archived => 0
+ },
+ {
+ eva => $s->{eva},
+ source => $opt{backend_id}
+ }
+ );
- if ( my $s = $self->get_by_eva( $loc->eva, db => $db ) ) {
- if ( $source == 1 and $s->{source} == 0 and not $s->{archived} ) {
+ # MOTIS backends do not provide a numeric ID, so we set our ID here.
+ $stop->{eva} = $s->{eva};
return;
}
- $db->update(
+
+ my $s = $opt{db}->query(
+ qq {
+ with new_station as (
+ insert into stations_external_ids (backend_id, external_id)
+ values (?, ?)
+ returning eva, backend_id
+ )
+
+ insert into stations (eva, name, lat, lon, source, archived)
+ values ((select eva from new_station), ?, ?, ?, (select backend_id from new_station), ?)
+ returning *
+ },
+ (
+ $opt{backend_id}, $stop->id, $stop->name,
+ $stop->lat, $stop->lon, 0,
+ )
+ );
+
+ # MOTIS backends do not provide a numeric ID, so we set our ID here.
+ $stop->{eva} = $s->hash->{eva};
+ return;
+ }
+
+ my $loc = $stop->loc;
+ if (
+ my $s = $self->get_by_eva(
+ $loc->eva,
+ db => $opt{db},
+ backend_id => $opt{backend_id}
+ )
+ )
+ {
+ $opt{db}->update(
'stations',
{
name => $loc->name,
lat => $loc->lat,
lon => $loc->lon,
- source => $source,
archived => 0
},
- { eva => $loc->eva }
+ {
+ eva => $loc->eva,
+ source => $opt{backend_id}
+ }
);
return;
}
- $db->insert(
+ $opt{db}->insert(
'stations',
{
eva => $loc->eva,
name => $loc->name,
lat => $loc->lat,
lon => $loc->lon,
- source => $source,
+ source => $opt{backend_id},
archived => 0
}
);
+
+ return;
}
sub add_meta {
my ( $self, %opt ) = @_;
- my $db = $opt{db} // $self->{pg}->db;
my $eva = $opt{eva};
my @meta = @{ $opt{meta} };
+ $opt{db} //= $self->{pg}->db;
+ $opt{backend_id} //= $self->get_backend_id(%opt);
+
for my $meta (@meta) {
if ( $meta != $eva ) {
- $db->insert(
+ $opt{db}->insert(
'related_stations',
{
- eva => $eva,
- meta => $meta
+ eva => $eva,
+ meta => $meta,
+ backend_id => $opt{backend_id},
},
{ on_conflict => undef }
);
@@ -74,7 +338,7 @@ sub add_meta {
sub get_db_iterator {
my ($self) = @_;
- return $self->{pg}->db->select( 'stations', '*' );
+ return $self->{pg}->db->select( 'stations_str', '*' );
}
sub get_meta {
@@ -82,7 +346,16 @@ sub get_meta {
my $db = $opt{db} // $self->{pg}->db;
my $eva = $opt{eva};
- my $res = $db->select( 'related_stations', ['meta'], { eva => $eva } );
+ $opt{backend_id} //= $self->get_backend_id( %opt, db => $db );
+
+ my $res = $db->select(
+ 'related_stations',
+ ['meta'],
+ {
+ eva => $eva,
+ backend_id => $opt{backend_id}
+ }
+ );
my @ret;
while ( my $row = $res->hash ) {
@@ -93,9 +366,12 @@ sub get_meta {
}
sub get_for_autocomplete {
- my ($self) = @_;
+ my ( $self, %opt ) = @_;
- my $res = $self->{pg}->db->select( 'stations', ['name'] );
+ $opt{backend_id} //= $self->get_backend_id(%opt);
+
+ my $res = $self->{pg}
+ ->db->select( 'stations', ['name'], { source => $opt{backend_id} } );
my %ret;
while ( my $row = $res->hash ) {
@@ -113,43 +389,74 @@ sub get_by_eva {
return;
}
- my $db = $opt{db} // $self->{pg}->db;
+ $opt{db} //= $self->{pg}->db;
+ $opt{backend_id} //= $self->get_backend_id(%opt);
- return $db->select( 'stations', '*', { eva => $eva } )->hash;
+ return $opt{db}->select(
+ 'stations',
+ '*',
+ {
+ eva => $eva,
+ source => $opt{backend_id}
+ }
+ )->hash;
}
-# Fast
-sub get_by_evas {
- my ( $self, @evas ) = @_;
+# Slow
+sub get_by_external_id {
+ my ( $self, %opt ) = @_;
- my @ret
- = $self->{pg}->db->select( 'stations', '*', { eva => { '=', \@evas } } )
- ->hashes->each;
- return @ret;
+ if ( not $opt{external_id} ) {
+ return;
+ }
+
+ $opt{db} //= $self->{pg}->db;
+ $opt{backend_id} //= $self->get_backend_id(%opt);
+
+ return $opt{db}->select(
+ 'stations_with_external_ids',
+ '*',
+ {
+ external_id => $opt{external_id},
+ source => $opt{backend_id},
+ }
+ )->hash;
}
-# Slow
-sub get_latlon_by_name {
+# Fast
+sub get_by_evas {
my ( $self, %opt ) = @_;
- my $db = $opt{db} // $self->{pg}->db;
+ $opt{db} //= $self->{pg}->db;
+ $opt{backend_id} //= $self->get_backend_id(%opt);
- my %location;
- my $res = $db->select( 'stations', [ 'name', 'lat', 'lon' ] );
- while ( my $row = $res->hash ) {
- $location{ $row->{name} } = [ $row->{lat}, $row->{lon} ];
- }
- return \%location;
+ my @ret = $self->{pg}->db->select(
+ 'stations',
+ '*',
+ {
+ eva => { '=', $opt{evas} },
+ source => $opt{backend_id}
+ }
+ )->hashes->each;
+ return @ret;
}
# Slow
sub get_by_name {
my ( $self, $name, %opt ) = @_;
- my $db = $opt{db} // $self->{pg}->db;
+ $opt{db} //= $self->{pg}->db;
+ $opt{backend_id} //= $self->get_backend_id(%opt);
- return $db->select( 'stations', '*', { name => $name }, { limit => 1 } )
- ->hash;
+ return $opt{db}->select(
+ 'stations',
+ '*',
+ {
+ name => $name,
+ source => $opt{backend_id}
+ },
+ { limit => 1 }
+ )->hash;
}
# Slow
@@ -166,16 +473,27 @@ sub get_by_names {
sub get_by_ds100 {
my ( $self, $ds100, %opt ) = @_;
- my $db = $opt{db} // $self->{pg}->db;
+ $opt{db} //= $self->{pg}->db;
+ $opt{backend_id} //= $self->get_backend_id(%opt);
- return $db->select( 'stations', '*', { ds100 => $ds100 }, { limit => 1 } )
- ->hash;
+ return $opt{db}->select(
+ 'stations',
+ '*',
+ {
+ ds100 => $ds100,
+ source => $opt{backend_id}
+ },
+ { limit => 1 }
+ )->hash;
}
# Can be slow
sub search {
my ( $self, $identifier, %opt ) = @_;
+ $opt{db} //= $self->{pg}->db;
+ $opt{backend_id} //= $self->get_backend_id(%opt);
+
if ( $identifier =~ m{ ^ \d+ $ }x ) {
return $self->get_by_eva( $identifier, %opt )
// $self->get_by_ds100( $identifier, %opt )
diff --git a/lib/Travelynx/Model/Traewelling.pm b/lib/Travelynx/Model/Traewelling.pm
index 25648cc..608da15 100644
--- a/lib/Travelynx/Model/Traewelling.pm
+++ b/lib/Travelynx/Model/Traewelling.pm
@@ -224,6 +224,7 @@ sub get_pushable_accounts {
join in_transit_str as i on t.user_id = i.user_id
where t.push_sync = True
and i.arr_eva is not null
+ and i.backend_id = (select id from backends where dbris = true and name = 'bahn.de')
and i.cancelled = False
}
);
diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm
index 4602fa2..be9e80b 100644
--- a/lib/Travelynx/Model/Users.pm
+++ b/lib/Travelynx/Model/Users.pm
@@ -40,14 +40,6 @@ my %predicate_atoi = (
is_blocked_by => 3,
);
-my @sb_templates = (
- undef,
- [ 'DBF', 'https://dbf.finalrewind.org/{name}?rt=1#{tt}{tn}' ],
- [ 'bahn.expert', 'https://bahn.expert/{name}#{id}' ],
- [ 'DBF HAFAS', 'https://dbf.finalrewind.org/{name}?rt=1&hafas=1#{tt}{tn}' ],
- [ 'bahn.expert/regional', 'https://bahn.expert/regional/{name}#{id}' ],
-);
-
my %token_id = (
status => 1,
history => 2,
@@ -213,6 +205,17 @@ sub get_privacy_by {
return;
}
+sub set_backend {
+ my ( $self, %opt ) = @_;
+ $opt{db} //= $self->{pg}->db;
+
+ $opt{db}->update(
+ 'users',
+ { backend_id => $opt{backend_id} },
+ { id => $opt{uid} }
+ );
+}
+
sub set_privacy {
my ( $self, %opt ) = @_;
my $db = $opt{db} // $self->{pg}->db;
@@ -409,12 +412,13 @@ sub get {
my $uid = $opt{uid};
my $user = $db->select(
- 'users',
+ 'users_with_backend',
'id, name, status, public_level, email, '
- . 'external_services, accept_follows, notifications, '
+ . 'accept_follows, notifications, '
. 'extract(epoch from registered_at) as registered_at_ts, '
. 'extract(epoch from last_seen) as last_seen_ts, '
- . 'extract(epoch from deletion_requested) as deletion_requested_ts',
+ . 'extract(epoch from deletion_requested) as deletion_requested_ts, '
+ . 'backend_id, backend_name, dbris, efa, hafas, motis',
{ id => $uid }
)->hash;
if ($user) {
@@ -435,12 +439,8 @@ sub get {
past_status => $user->{public_level} & 0x08000 ? 1 : 0,
past_all => $user->{public_level} & 0x10000 ? 1 : 0,
email => $user->{email},
- sb_name => $user->{external_services}
- ? $sb_templates[ $user->{external_services} & 0x07 ][0]
- : undef,
- sb_template => $user->{external_services}
- ? $sb_templates[ $user->{external_services} & 0x07 ][1]
- : undef,
+ sb_template =>
+'https://dbf.finalrewind.org/{name}?dbris={dbris}&efa={efa}&hafas={hafas}&motis={motis}#{id_or_tttn}',
registered_at => DateTime->from_epoch(
epoch => $user->{registered_at_ts},
time_zone => 'Europe/Berlin'
@@ -455,6 +455,12 @@ sub get {
time_zone => 'Europe/Berlin'
)
: undef,
+ backend_id => $user->{backend_id},
+ backend_name => $user->{backend_name},
+ backend_dbris => $user->{dbris},
+ backend_efa => $user->{efa},
+ backend_hafas => $user->{hafas},
+ backend_motis => $user->{motis},
};
}
return undef;
@@ -659,24 +665,6 @@ sub use_history {
}
}
-sub use_external_services {
- my ( $self, %opt ) = @_;
- my $db = $opt{db} // $self->{pg}->db;
- my $uid = $opt{uid};
- my $value = $opt{set};
-
- if ( defined $value ) {
- if ( $value < 0 or $value > 4 ) {
- $value = 0;
- }
- $db->update( 'users', { external_services => $value }, { id => $uid } );
- }
- else {
- return $db->select( 'users', ['external_services'], { id => $uid } )
- ->hash->{external_services};
- }
-}
-
sub get_webhook {
my ( $self, %opt ) = @_;
my $db = $opt{db} // $self->{pg}->db;
diff --git a/public/service-worker.js b/public/service-worker.js
index 30e49d7..b8bb623 100644
--- a/public/service-worker.js
+++ b/public/service-worker.js
@@ -1,18 +1,17 @@
-const CACHE_NAME = 'static-cache-v71';
+const CACHE_NAME = 'static-cache-v97';
const FILES_TO_CACHE = [
'/favicon.ico',
'/offline.html',
- '/static/v71/css/light.min.css',
- '/static/v71/css/dark.min.css',
- '/static/v71/css/material-icons.css',
- '/static/v71/fonts/MaterialIcons-Regular.woff2',
- '/static/v71/fonts/MaterialIcons-Regular.woff',
- '/static/v71/fonts/MaterialIcons-Regular.ttf',
- '/static/v71/js/jquery-3.4.1.min.js',
- '/static/v71/js/materialize.min.js',
- '/static/v71/js/travelynx-actions.min.js',
- '/static/v71/js/autocomplete.min.js',
- '/static/v71/js/geolocation.min.js',
+ '/static/v97/css/light.min.css',
+ '/static/v97/css/dark.min.css',
+ '/static/v97/css/material-icons.css',
+ '/static/v97/fonts/MaterialIcons-Regular.woff2',
+ '/static/v97/fonts/MaterialIcons-Regular.woff',
+ '/static/v97/fonts/MaterialIcons-Regular.ttf',
+ '/static/v97/js/jquery-3.4.1.min.js',
+ '/static/v97/js/materialize.min.js',
+ '/static/v97/js/travelynx-actions.min.js',
+ '/static/v97/js/geolocation.min.js',
];
self.addEventListener('install', (evt) => {
diff --git a/public/static/css/dark.min.css b/public/static/css/dark.min.css
index 3594ca3..ca04ae6 100644
--- a/public/static/css/dark.min.css
+++ b/public/static/css/dark.min.css
@@ -5,4 +5,4 @@
* Copyright 2014 Alfiana E. Sibuea and other contributors
* Released under the MIT license
* https://github.com/fians/Waves/blob/master/LICENSE
- */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);transition:all 0.7s ease-out;transition-property:transform, opacity;transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none !important}.waves-circle{transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width : 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:0.8rem;transform:translateY(-140%)}.materialboxed{display:block;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #98f5ff}button:focus{outline:none;background-color:#00a9bb}label{font-size:.8rem;color:#fff}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;box-shadow:none;box-sizing:content-box;transition:box-shadow .3s, border .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #0097a7;box-shadow:0 1px 0 0 #0097a7}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#0097a7}input:not([type]):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#388E3C}input:not([type]):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#D32F2F}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}.select-wrapper.valid>input.select-dropdown,input.valid:not([type]),input.valid:not([type]):focus,input[type=text].valid:not(.browser-default),input[type=text].valid:not(.browser-default):focus,input[type=password].valid:not(.browser-default),input[type=password].valid:not(.browser-default):focus,input[type=email].valid:not(.browser-default),input[type=email].valid:not(.browser-default):focus,input[type=url].valid:not(.browser-default),input[type=url].valid:not(.browser-default):focus,input[type=time].valid:not(.browser-default),input[type=time].valid:not(.browser-default):focus,input[type=date].valid:not(.browser-default),input[type=date].valid:not(.browser-default):focus,input[type=datetime].valid:not(.browser-default),input[type=datetime].valid:not(.browser-default):focus,input[type=datetime-local].valid:not(.browser-default),input[type=datetime-local].valid:not(.browser-default):focus,input[type=tel].valid:not(.browser-default),input[type=tel].valid:not(.browser-default):focus,input[type=number].valid:not(.browser-default),input[type=number].valid:not(.browser-default):focus,input[type=search].valid:not(.browser-default),input[type=search].valid:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus{border-bottom:1px solid #388E3C;box-shadow:0 1px 0 0 #388E3C}.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus,input.invalid:not([type]),input.invalid:not([type]):focus,input[type=text].invalid:not(.browser-default),input[type=text].invalid:not(.browser-default):focus,input[type=password].invalid:not(.browser-default),input[type=password].invalid:not(.browser-default):focus,input[type=email].invalid:not(.browser-default),input[type=email].invalid:not(.browser-default):focus,input[type=url].invalid:not(.browser-default),input[type=url].invalid:not(.browser-default):focus,input[type=time].invalid:not(.browser-default),input[type=time].invalid:not(.browser-default):focus,input[type=date].invalid:not(.browser-default),input[type=date].invalid:not(.browser-default):focus,input[type=datetime].invalid:not(.browser-default),input[type=datetime].invalid:not(.browser-default):focus,input[type=datetime-local].invalid:not(.browser-default),input[type=datetime-local].invalid:not(.browser-default):focus,input[type=tel].invalid:not(.browser-default),input[type=tel].invalid:not(.browser-default):focus,input[type=number].invalid:not(.browser-default),input[type=number].invalid:not(.browser-default):focus,input[type=search].invalid:not(.browser-default),input[type=search].invalid:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus{border-bottom:1px solid #D32F2F;box-shadow:0 1px 0 0 #D32F2F}.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid~.helper-text[data-error],input.valid:not([type])~.helper-text[data-success],input.valid:not([type]):focus~.helper-text[data-success],input.invalid:not([type])~.helper-text[data-error],input.invalid:not([type]):focus~.helper-text[data-error],input[type=text].valid:not(.browser-default)~.helper-text[data-success],input[type=text].invalid:not(.browser-default)~.helper-text[data-error],input[type=password].valid:not(.browser-default)~.helper-text[data-success],input[type=password].invalid:not(.browser-default)~.helper-text[data-error],input[type=email].valid:not(.browser-default)~.helper-text[data-success],input[type=email].invalid:not(.browser-default)~.helper-text[data-error],input[type=url].valid:not(.browser-default)~.helper-text[data-success],input[type=url].invalid:not(.browser-default)~.helper-text[data-error],input[type=time].valid:not(.browser-default)~.helper-text[data-success],input[type=time].invalid:not(.browser-default)~.helper-text[data-error],input[type=date].valid:not(.browser-default)~.helper-text[data-success],input[type=date].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime-local].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime-local].invalid:not(.browser-default)~.helper-text[data-error],input[type=tel].valid:not(.browser-default)~.helper-text[data-success],input[type=tel].invalid:not(.browser-default)~.helper-text[data-error],input[type=number].valid:not(.browser-default)~.helper-text[data-success],input[type=number].invalid:not(.browser-default)~.helper-text[data-error],input[type=search].valid:not(.browser-default)~.helper-text[data-success],input[type=search].invalid:not(.browser-default)~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea.valid:focus~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.invalid:focus~.helper-text[data-error]{color:transparent;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input.valid:not([type])~.helper-text:after,input.valid:not([type]):focus~.helper-text:after,input[type=text].valid:not(.browser-default)~.helper-text:after,input[type=text].valid:not(.browser-default):focus~.helper-text:after,input[type=password].valid:not(.browser-default)~.helper-text:after,input[type=password].valid:not(.browser-default):focus~.helper-text:after,input[type=email].valid:not(.browser-default)~.helper-text:after,input[type=email].valid:not(.browser-default):focus~.helper-text:after,input[type=url].valid:not(.browser-default)~.helper-text:after,input[type=url].valid:not(.browser-default):focus~.helper-text:after,input[type=time].valid:not(.browser-default)~.helper-text:after,input[type=time].valid:not(.browser-default):focus~.helper-text:after,input[type=date].valid:not(.browser-default)~.helper-text:after,input[type=date].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime].valid:not(.browser-default)~.helper-text:after,input[type=datetime].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].valid:not(.browser-default)~.helper-text:after,input[type=datetime-local].valid:not(.browser-default):focus~.helper-text:after,input[type=tel].valid:not(.browser-default)~.helper-text:after,input[type=tel].valid:not(.browser-default):focus~.helper-text:after,input[type=number].valid:not(.browser-default)~.helper-text:after,input[type=number].valid:not(.browser-default):focus~.helper-text:after,input[type=search].valid:not(.browser-default)~.helper-text:after,input[type=search].valid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea.valid:focus~.helper-text:after{content:attr(data-success);color:#388E3C}.select-wrapper.invalid~.helper-text:after,input.invalid:not([type])~.helper-text:after,input.invalid:not([type]):focus~.helper-text:after,input[type=text].invalid:not(.browser-default)~.helper-text:after,input[type=text].invalid:not(.browser-default):focus~.helper-text:after,input[type=password].invalid:not(.browser-default)~.helper-text:after,input[type=password].invalid:not(.browser-default):focus~.helper-text:after,input[type=email].invalid:not(.browser-default)~.helper-text:after,input[type=email].invalid:not(.browser-default):focus~.helper-text:after,input[type=url].invalid:not(.browser-default)~.helper-text:after,input[type=url].invalid:not(.browser-default):focus~.helper-text:after,input[type=time].invalid:not(.browser-default)~.helper-text:after,input[type=time].invalid:not(.browser-default):focus~.helper-text:after,input[type=date].invalid:not(.browser-default)~.helper-text:after,input[type=date].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime].invalid:not(.browser-default)~.helper-text:after,input[type=datetime].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default)~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default):focus~.helper-text:after,input[type=tel].invalid:not(.browser-default)~.helper-text:after,input[type=tel].invalid:not(.browser-default):focus~.helper-text:after,input[type=number].invalid:not(.browser-default)~.helper-text:after,input[type=number].invalid:not(.browser-default):focus~.helper-text:after,input[type=search].invalid:not(.browser-default)~.helper-text:after,input[type=search].invalid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea.invalid:focus~.helper-text:after{content:attr(data-error);color:#D32F2F}.select-wrapper+label:after,input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~label,.input-field.col .prefix~.validate~label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#fff;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:transform .2s ease-out, color .2s ease-out;transform-origin:0% 100%;text-align:initial;transform:translateY(12px)}.input-field>label:not(.label-icon).active{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#0097a7}.input-field .prefix~input,.input-field .prefix~textarea,.input-field .prefix~label,.input-field .prefix~.validate~label,.input-field .prefix~.helper-text,.input-field .prefix~.autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width : 992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width : 600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default)~.material-icons{color:#444}.input-field input[type=search]+.label-icon{transform:none;left:1rem}.input-field input[type=search]~.mdi-navigation-close,.input-field input[type=search]~.material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #fff}[type="radio"]:not(:checked)+span:after{transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #0097a7}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#0097a7}[type="radio"]:checked+span:after{transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #fff;border-radius:1px;margin-top:3px;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #0097a7;border-bottom:2px solid #0097a7;transform:rotate(40deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #0097a7;border-bottom:none;transform:rotate(90deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #fff;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #0097a7;background-color:#0097a7;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#fff;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#0097a7;border-color:#0097a7}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#42d5e4}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#0097a7}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease}.switch label .lever:before{background-color:rgba(0,151,167,0.15)}.switch label .lever:after{background-color:#F1F1F1;box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled)~.lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,151,167,0.15)}input[type=checkbox]:not(:disabled)~.lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #0097a7}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#0097a7;margin-left:7px;transform-origin:50% 50%;transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#0097a7;font-size:0;transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s;-webkit-appearance:none;background-color:#0097a7;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #8b1014}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #8b1014}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;backface-visibility:hidden;transform:translateX(-105%)}.sidenav.right-aligned{right:0;transform:translateX(105%);left:auto;transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#fff}.sidenav li>a.btn:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#00aec1}.sidenav li>a.btn-floating:hover{background-color:#0097a7}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width : 992px){.sidenav.sidenav-fixed{transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#8b1014}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#0097a7}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;perspective:500px;transform-style:preserve-3d;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:#757575;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{transform:scale(1);opacity:.95;transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;transition:opacity .3s, transform .3s, visibility 0s 1s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#8b1014;box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;transform:scale(0);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{transform:scale(0);transition:transform .3s}.tap-target-wave::after{visibility:hidden;transition:opacity .3s, transform .3s, visibility 0s;z-index:-1}.tap-target-origin{top:50%;left:50%;transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s, transform .3s;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@keyframes pulse-animation{0%{opacity:1;transform:scale(1)}50%{opacity:0;transform:scale(1.5)}100%{opacity:0;transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.datepicker-controls{display:flex;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{flex:1 auto;background-color:#0097a7;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#0097a7}.datepicker-table td.is-selected{background-color:#0097a7;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(4,148,163,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#0097a7;padding:0 1rem}.datepicker-clear{color:#D32F2F}@media only screen and (min-width : 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{flex-direction:row}.datepicker-date-display{flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{flex:1 auto;background-color:#0097a7;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(0,151,167,0.25)}.timepicker-dial{transition:transform 350ms, opacity 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{transform:scale(0.8, 0.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#0097a7;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#0097a7}.timepicker-canvas-bg{stroke:none;fill:#0097a7}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:flex;justify-content:space-between}.timepicker-clear{color:#D32F2F}.timepicker-close{color:#0097a7}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width : 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}.caution-color{background-color:#D32F2F}.caution-color-text{color:#D32F2F}.info-color{background-color:#263238}.info-color-text{color:#263238}.contrast-color{background-color:#fff}.contrast-color-text{color:#fff}.success-color{background-color:#388E3C}.success-color-text{color:#388E3C}html{background-color:#101010}a.unmarked{color:#fff}.white-text a{color:#eeeeff}div.targetlist{display:grid;grid-template-columns:1fr max-content;align-items:center}div.targetlist>a.nonflex{padding-left:1em;padding-top:1em;padding-bottom:1em;display:inline-block}a.tablerow{display:flex;justify-content:space-between;padding-top:1em;padding-bottom:1em;border-bottom:1px solid rgba(0,0,0,0.12)}a.tablerow .material-icons{vertical-align:bottom;margin-bottom:0.2em}a.tablerow span{display:inline-block}.pagination li a{color:#fff}.collection-item{color:#fff}.collection-item .secondary-content{color:#fff}.collection-item.disabled{color:#757575}.action-checkin,.action-checkout,.action-undo,.action-cancelled-from,.action-cancelled-to,.action-share{cursor:pointer}.config a{cursor:pointer}.navbar-fixed{z-index:1001}.brand-logo span{transition:color 1s}.brand-logo:hover .ca,.brand-logo:hover .ce{color:#a8e3fa !important}.brand-logo:hover .cb,.brand-logo:hover .cd{color:#f5c4ce !important}.wagons span{margin-right:0.5ex;color:#808080}.wagons .wagonclass{font-weight:bold;color:inherit}.wagons .wagonnum{margin-right:0;color:inherit}.wagons .checksum:before{content:"-"}h1{font-size:2.92rem;margin:1.9466666667rem 0 1.168rem 0}h2{font-size:2.28rem;margin:1.52rem 0 .912rem 0}h3{font-size:1.64rem;margin:1.0933333333rem 0 .656rem 0}.geolocation i.material-icons{font-size:16px}ul.suggestions li{padding-top:0.5rem;padding-bottom:0.5rem}.collection.departures>li,.collection.history>li{transition:background .3s;display:grid}.collection.departures>li:not(#now,.history-date-change):hover,.collection.departures>li:focus-within,.collection.history>li:not(#now,.history-date-change):hover,.collection.history>li:focus-within{background-color:#212121;outline:2px solid #039be5}.collection.departures li{grid-template-columns:10ch 10ch 1fr;align-items:center}.collection.departures li#now{background-color:#212121;padding:2rem 20px;grid-template-columns:10ch 1fr}.collection.departures li#now strong{font-weight:bold}.collection.departures li.cancelled{background-color:#702020;font-style:italic}.collection.departures li.cancelled .dep-line{background-color:transparent;border:1px solid;color:#fff}.collection.departures li.cancelled .dep-time::after{content:" ⊖";font-style:normal}.departures .dep-time{color:#fff}.departures .dep-time:focus{outline:none}.departures .dep-dest{margin-left:0.8rem}.departures .dep-dest .followee-checkin{font-size:0.9rem;display:block}.departures .dep-dest .followee-checkin i.material-icons{vertical-align:middle}.collection.history>li{display:grid;grid-template-columns:10ch 1fr;grid-template-rows:1fr}.collection.history>li a:first-child{align-self:center;text-align:center;display:flex}.collection.history>li.history-date-change{display:block;font-weight:bold}ul.route-history>li{list-style:none;position:relative;display:grid;grid-template-columns:1rem 1fr;gap:0.5rem}ul.route-history>li a{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul.route-history>li strong{font-weight:600}ul.route-history>li i.material-icons[aria-label=nach]{padding-top:0.4rem}ul.route-history>li i.material-icons[aria-label=von]{display:block;transform:rotate(-90deg);height:1rem;margin-top:0.4rem}ul.route-history>li::before{content:'';background:#fff;position:absolute;width:2px;left:calc( (1rem - 2px) / 2);bottom:0;top:0}ul.route-history>li:first-of-type::before{top:1.3rem}ul.route-history>li:last-of-type::before{bottom:unset;height:0.5rem}.dep-line{text-align:center;padding:.2rem;color:white;background:#424242;border-radius:.2rem;display:inline-block;font-weight:600;line-height:1;height:fit-content;width:fit-content;min-width:6ch;margin:0 auto}.dep-line.Bus,.dep-line.RUF,.dep-line.AST{background-color:#a3167e;border-radius:5rem;padding:.2rem .5rem}.dep-line.STR{background-color:#c5161c;border-radius:5rem;padding:.2rem .5rem}.dep-line.S,.dep-line.RS,.dep-line.RER,.dep-line.SKW{background-color:#008d4f;border-radius:5rem;padding:.2rem .5rem}.dep-line.U,.dep-line.STB,.dep-line.M{background-color:#014e8d;border-radius:5rem;padding:.2rem .5rem}.dep-line.RE,.dep-line.IRE,.dep-line.REX{background-color:#ff4f00}.dep-line.RB,.dep-line.MEX,.dep-line.TER,.dep-line.R{background-color:#1f4a87}.dep-line.IC,.dep-line.ICE,.dep-line.EC,.dep-line.ECE,.dep-line.D,.dep-line.IR,.dep-line.TGV,.dep-line.OGV,.dep-line.EST,.dep-line.TLK,.dep-line.EIC{background-color:#ff0404;font-weight:900;font-style:italic;padding:.2rem}.dep-line.RJ,.dep-line.RJX{background-color:#c63131}.dep-line.NJ,.dep-line.EN{background-color:#29255b}.dep-line.WB{background-color:#2e85ce}.dep-line.FLX{background-color:#71d800;color:black}.departures.connections li{grid-template-columns:15ch 10ch 1fr}.departures.connections .connect-platform-wrapper{text-align:center}.departures.connections .connect-platform-wrapper span{display:block}@media screen and (max-width: 600px){.collection.departures li{grid-template-columns:10ch 1fr}.collection.departures li .dep-line,.collection.departures li .dep-time,.collection.departures li .connect-platform-wrapper{grid-column:1;text-align:center}.collection.departures li .dep-dest{grid-column:2;grid-row:1 / span 2}.departures.connections li{grid-template-columns:15ch 1fr}.departures.connections li .connect-platform-wrapper span{display:inline-block}}.progress{background-color:#424242}.progress>.determinate{background-color:#2196F3}html{background-color:#101010}input[type=email],input[type=text],input[type=password],textarea{color:#fff}
+ */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);transition:all 0.7s ease-out;transition-property:transform, opacity;transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none !important}.waves-circle{transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width : 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:0.8rem;transform:translateY(-140%)}.materialboxed{display:block;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #98f5ff}button:focus{outline:none;background-color:#00a9bb}label{font-size:.8rem;color:#fff}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;box-shadow:none;box-sizing:content-box;transition:box-shadow .3s, border .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #0097a7;box-shadow:0 1px 0 0 #0097a7}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#0097a7}input:not([type]):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#388E3C}input:not([type]):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#D32F2F}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}.select-wrapper.valid>input.select-dropdown,input.valid:not([type]),input.valid:not([type]):focus,input[type=text].valid:not(.browser-default),input[type=text].valid:not(.browser-default):focus,input[type=password].valid:not(.browser-default),input[type=password].valid:not(.browser-default):focus,input[type=email].valid:not(.browser-default),input[type=email].valid:not(.browser-default):focus,input[type=url].valid:not(.browser-default),input[type=url].valid:not(.browser-default):focus,input[type=time].valid:not(.browser-default),input[type=time].valid:not(.browser-default):focus,input[type=date].valid:not(.browser-default),input[type=date].valid:not(.browser-default):focus,input[type=datetime].valid:not(.browser-default),input[type=datetime].valid:not(.browser-default):focus,input[type=datetime-local].valid:not(.browser-default),input[type=datetime-local].valid:not(.browser-default):focus,input[type=tel].valid:not(.browser-default),input[type=tel].valid:not(.browser-default):focus,input[type=number].valid:not(.browser-default),input[type=number].valid:not(.browser-default):focus,input[type=search].valid:not(.browser-default),input[type=search].valid:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus{border-bottom:1px solid #388E3C;box-shadow:0 1px 0 0 #388E3C}.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus,input.invalid:not([type]),input.invalid:not([type]):focus,input[type=text].invalid:not(.browser-default),input[type=text].invalid:not(.browser-default):focus,input[type=password].invalid:not(.browser-default),input[type=password].invalid:not(.browser-default):focus,input[type=email].invalid:not(.browser-default),input[type=email].invalid:not(.browser-default):focus,input[type=url].invalid:not(.browser-default),input[type=url].invalid:not(.browser-default):focus,input[type=time].invalid:not(.browser-default),input[type=time].invalid:not(.browser-default):focus,input[type=date].invalid:not(.browser-default),input[type=date].invalid:not(.browser-default):focus,input[type=datetime].invalid:not(.browser-default),input[type=datetime].invalid:not(.browser-default):focus,input[type=datetime-local].invalid:not(.browser-default),input[type=datetime-local].invalid:not(.browser-default):focus,input[type=tel].invalid:not(.browser-default),input[type=tel].invalid:not(.browser-default):focus,input[type=number].invalid:not(.browser-default),input[type=number].invalid:not(.browser-default):focus,input[type=search].invalid:not(.browser-default),input[type=search].invalid:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus{border-bottom:1px solid #D32F2F;box-shadow:0 1px 0 0 #D32F2F}.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid~.helper-text[data-error],input.valid:not([type])~.helper-text[data-success],input.valid:not([type]):focus~.helper-text[data-success],input.invalid:not([type])~.helper-text[data-error],input.invalid:not([type]):focus~.helper-text[data-error],input[type=text].valid:not(.browser-default)~.helper-text[data-success],input[type=text].invalid:not(.browser-default)~.helper-text[data-error],input[type=password].valid:not(.browser-default)~.helper-text[data-success],input[type=password].invalid:not(.browser-default)~.helper-text[data-error],input[type=email].valid:not(.browser-default)~.helper-text[data-success],input[type=email].invalid:not(.browser-default)~.helper-text[data-error],input[type=url].valid:not(.browser-default)~.helper-text[data-success],input[type=url].invalid:not(.browser-default)~.helper-text[data-error],input[type=time].valid:not(.browser-default)~.helper-text[data-success],input[type=time].invalid:not(.browser-default)~.helper-text[data-error],input[type=date].valid:not(.browser-default)~.helper-text[data-success],input[type=date].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime-local].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime-local].invalid:not(.browser-default)~.helper-text[data-error],input[type=tel].valid:not(.browser-default)~.helper-text[data-success],input[type=tel].invalid:not(.browser-default)~.helper-text[data-error],input[type=number].valid:not(.browser-default)~.helper-text[data-success],input[type=number].invalid:not(.browser-default)~.helper-text[data-error],input[type=search].valid:not(.browser-default)~.helper-text[data-success],input[type=search].invalid:not(.browser-default)~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea.valid:focus~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.invalid:focus~.helper-text[data-error]{color:transparent;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input.valid:not([type])~.helper-text:after,input.valid:not([type]):focus~.helper-text:after,input[type=text].valid:not(.browser-default)~.helper-text:after,input[type=text].valid:not(.browser-default):focus~.helper-text:after,input[type=password].valid:not(.browser-default)~.helper-text:after,input[type=password].valid:not(.browser-default):focus~.helper-text:after,input[type=email].valid:not(.browser-default)~.helper-text:after,input[type=email].valid:not(.browser-default):focus~.helper-text:after,input[type=url].valid:not(.browser-default)~.helper-text:after,input[type=url].valid:not(.browser-default):focus~.helper-text:after,input[type=time].valid:not(.browser-default)~.helper-text:after,input[type=time].valid:not(.browser-default):focus~.helper-text:after,input[type=date].valid:not(.browser-default)~.helper-text:after,input[type=date].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime].valid:not(.browser-default)~.helper-text:after,input[type=datetime].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].valid:not(.browser-default)~.helper-text:after,input[type=datetime-local].valid:not(.browser-default):focus~.helper-text:after,input[type=tel].valid:not(.browser-default)~.helper-text:after,input[type=tel].valid:not(.browser-default):focus~.helper-text:after,input[type=number].valid:not(.browser-default)~.helper-text:after,input[type=number].valid:not(.browser-default):focus~.helper-text:after,input[type=search].valid:not(.browser-default)~.helper-text:after,input[type=search].valid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea.valid:focus~.helper-text:after{content:attr(data-success);color:#388E3C}.select-wrapper.invalid~.helper-text:after,input.invalid:not([type])~.helper-text:after,input.invalid:not([type]):focus~.helper-text:after,input[type=text].invalid:not(.browser-default)~.helper-text:after,input[type=text].invalid:not(.browser-default):focus~.helper-text:after,input[type=password].invalid:not(.browser-default)~.helper-text:after,input[type=password].invalid:not(.browser-default):focus~.helper-text:after,input[type=email].invalid:not(.browser-default)~.helper-text:after,input[type=email].invalid:not(.browser-default):focus~.helper-text:after,input[type=url].invalid:not(.browser-default)~.helper-text:after,input[type=url].invalid:not(.browser-default):focus~.helper-text:after,input[type=time].invalid:not(.browser-default)~.helper-text:after,input[type=time].invalid:not(.browser-default):focus~.helper-text:after,input[type=date].invalid:not(.browser-default)~.helper-text:after,input[type=date].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime].invalid:not(.browser-default)~.helper-text:after,input[type=datetime].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default)~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default):focus~.helper-text:after,input[type=tel].invalid:not(.browser-default)~.helper-text:after,input[type=tel].invalid:not(.browser-default):focus~.helper-text:after,input[type=number].invalid:not(.browser-default)~.helper-text:after,input[type=number].invalid:not(.browser-default):focus~.helper-text:after,input[type=search].invalid:not(.browser-default)~.helper-text:after,input[type=search].invalid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea.invalid:focus~.helper-text:after{content:attr(data-error);color:#D32F2F}.select-wrapper+label:after,input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~label,.input-field.col .prefix~.validate~label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#fff;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:transform .2s ease-out, color .2s ease-out;transform-origin:0% 100%;text-align:initial;transform:translateY(12px)}.input-field>label:not(.label-icon).active{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#0097a7}.input-field .prefix~input,.input-field .prefix~textarea,.input-field .prefix~label,.input-field .prefix~.validate~label,.input-field .prefix~.helper-text,.input-field .prefix~.autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width : 992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width : 600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default)~.material-icons{color:#444}.input-field input[type=search]+.label-icon{transform:none;left:1rem}.input-field input[type=search]~.mdi-navigation-close,.input-field input[type=search]~.material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #fff}[type="radio"]:not(:checked)+span:after{transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #0097a7}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#0097a7}[type="radio"]:checked+span:after{transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #fff;border-radius:1px;margin-top:3px;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #0097a7;border-bottom:2px solid #0097a7;transform:rotate(40deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #0097a7;border-bottom:none;transform:rotate(90deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #fff;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #0097a7;background-color:#0097a7;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#fff;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#0097a7;border-color:#0097a7}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#42d5e4}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#0097a7}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease}.switch label .lever:before{background-color:rgba(0,151,167,0.15)}.switch label .lever:after{background-color:#F1F1F1;box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled)~.lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,151,167,0.15)}input[type=checkbox]:not(:disabled)~.lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #0097a7}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#0097a7;margin-left:7px;transform-origin:50% 50%;transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#0097a7;font-size:0;transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s;-webkit-appearance:none;background-color:#0097a7;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#0097a7;transition:box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(0,151,167,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #8b1014}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #8b1014}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;backface-visibility:hidden;transform:translateX(-105%)}.sidenav.right-aligned{right:0;transform:translateX(105%);left:auto;transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#fff}.sidenav li>a.btn:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#00aec1}.sidenav li>a.btn-floating:hover{background-color:#0097a7}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width : 992px){.sidenav.sidenav-fixed{transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#8b1014}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#0097a7}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;perspective:500px;transform-style:preserve-3d;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:#757575;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:#fff}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{transform:scale(1);opacity:.95;transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;transition:opacity .3s, transform .3s, visibility 0s 1s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#8b1014;box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;transform:scale(0);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{transform:scale(0);transition:transform .3s}.tap-target-wave::after{visibility:hidden;transition:opacity .3s, transform .3s, visibility 0s;z-index:-1}.tap-target-origin{top:50%;left:50%;transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s, transform .3s;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@keyframes pulse-animation{0%{opacity:1;transform:scale(1)}50%{opacity:0;transform:scale(1.5)}100%{opacity:0;transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.datepicker-controls{display:flex;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{flex:1 auto;background-color:#0097a7;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#0097a7}.datepicker-table td.is-selected{background-color:#0097a7;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(4,148,163,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#0097a7;padding:0 1rem}.datepicker-clear{color:#D32F2F}@media only screen and (min-width : 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{flex-direction:row}.datepicker-date-display{flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{flex:1 auto;background-color:#0097a7;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(0,151,167,0.25)}.timepicker-dial{transition:transform 350ms, opacity 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{transform:scale(0.8, 0.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#0097a7;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#0097a7}.timepicker-canvas-bg{stroke:none;fill:#0097a7}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:flex;justify-content:space-between}.timepicker-clear{color:#D32F2F}.timepicker-close{color:#0097a7}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width : 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}.caution-color{background-color:#D32F2F}.caution-color-text{color:#D32F2F}.info-color{background-color:#263238}.info-color-text{color:#263238}.contrast-color{background-color:#fff}.contrast-color-text{color:#fff}.success-color{background-color:#388E3C}.success-color-text{color:#388E3C}html{background-color:#101010}a.unmarked{color:#fff}.white-text a{color:#eeeeff}div.targetlist{display:grid;grid-template-columns:1fr max-content;align-items:center}div.targetlist>a.nonflex{padding-left:1em;padding-top:1em;padding-bottom:1em;display:inline-block}a.tablerow{display:flex;justify-content:space-between;padding-top:1em;padding-bottom:1em;border-bottom:1px solid rgba(0,0,0,0.12)}a.tablerow .material-icons{vertical-align:bottom;margin-bottom:0.2em}a.tablerow span{display:inline-block}.pagination li a{color:#fff}.collection-item{color:#fff}.collection-item .secondary-content{color:#fff}.collection-item.disabled{color:#757575}.action-checkin,.action-checkout,.action-undo,.action-cancelled-from,.action-cancelled-to,.action-share{cursor:pointer}.config a{cursor:pointer}.navbar-fixed{z-index:1001}.brand-logo span{transition:color 1s}.brand-logo:hover .ca,.brand-logo:hover .ce{color:#a8e3fa !important}.brand-logo:hover .cb,.brand-logo:hover .cd{color:#f5c4ce !important}.wagons span{margin-right:0.5ex;color:#808080}.wagons .wagonclass{font-weight:bold;color:inherit}.wagons .wagonnum{margin-right:0;color:inherit}.wagons .checksum:before{content:"-"}h1{font-size:2.92rem;margin:1.9466666667rem 0 1.168rem 0}h2{font-size:2.28rem;margin:1.52rem 0 .912rem 0}h3{font-size:1.64rem;margin:1.0933333333rem 0 .656rem 0}.geolocation i.material-icons{font-size:16px}ul.suggestions li{padding-top:0.5rem;padding-bottom:0.5rem}.collection.departures>li,.collection.history>li{transition:background .3s;display:grid}.collection.departures>li:not(#now,.history-date-change):hover,.collection.departures>li:focus-within,.collection.history>li:not(#now,.history-date-change):hover,.collection.history>li:focus-within{background-color:#212121;outline:2px solid #039be5}.collection.departures li{grid-template-columns:10ch 10ch 1fr;align-items:center}.collection.departures li#now{background-color:#212121;padding:2rem 20px;grid-template-columns:10ch 1fr}.collection.departures li#now strong{font-weight:bold}.collection.departures li.cancelled{background-color:#702020;font-style:italic}.collection.departures li.cancelled .dep-line{background-color:transparent;border:1px solid;color:#fff}.collection.departures li.cancelled .dep-time::after{content:" ⊖";font-style:normal}.departures .dep-time{color:#fff}.departures .dep-time:focus{outline:none}.departures .dep-dest{margin-left:0.8rem}.departures .dep-dest i.material-icons{vertical-align:middle}.departures .dep-dest .followee-checkin{font-size:0.9rem;display:block}.collection.history>li{display:grid;grid-template-columns:10ch 1fr;grid-template-rows:1fr}.collection.history>li a:first-child{align-self:center;text-align:center;display:flex}.collection.history>li.history-date-change{display:block;font-weight:bold}ul.route-history>li{list-style:none;position:relative;display:grid;grid-template-columns:1rem 1fr;gap:0.5rem}ul.route-history>li a{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul.route-history>li strong{font-weight:600}ul.route-history>li i.material-icons[aria-label=nach]{padding-top:0.4rem}ul.route-history>li i.material-icons[aria-label=von]{display:block;transform:rotate(-90deg);height:1rem;margin-top:0.4rem}ul.route-history>li::before{content:'';background:#fff;position:absolute;width:2px;left:calc( (1rem - 2px) / 2);bottom:0;top:0}ul.route-history>li:first-of-type::before{top:1.3rem}ul.route-history>li:last-of-type::before{bottom:unset;height:0.5rem}.dep-line{text-align:center;padding:.2rem;color:white;background:#424242;border-radius:.2rem;display:inline-block;font-weight:600;line-height:1;height:fit-content;width:fit-content;min-width:6ch;margin:0 auto}.dep-line.Bus,.dep-line.BUS,.dep-line.NachtBus,.dep-line.Niederflurbus,.dep-line.Stadtbus,.dep-line.MetroBus,.dep-line.PlusBus,.dep-line.Landbus,.dep-line.Regionalbus,.dep-line.RegionalBus,.dep-line.SB,.dep-line.ExpressBus,.dep-line.BSV,.dep-line.RVV-Bus-Linie,.dep-line.Buslinie,.dep-line.Omnibus,.dep-line.RegioBus{background-color:#a3167e;border-radius:5rem;padding:.2rem .5rem}.dep-line.RUF,.dep-line.AST,.dep-line.RufTaxi,.dep-line.Rufbus,.dep-line.Linientaxi{background-color:#ffd800;color:black;border-radius:5rem;padding:.2rem .5rem}.dep-line.Fhre,.dep-line.Fh,.dep-line.Schiff,.dep-line.SCH,.dep-line.KAT{background-color:#309fd1;border-radius:5rem;padding:.2rem .5rem}.dep-line.STR,.dep-line.Tram,.dep-line.TRAM,.dep-line.Str,.dep-line.Strb,.dep-line.STB,.dep-line.Straenbahn,.dep-line.NachtTram,.dep-line.Stadtbahn,.dep-line.Niederflurstrab{background-color:#c5161c;border-radius:5rem;padding:.2rem .5rem}.dep-line.S,.dep-line.RS,.dep-line.RER,.dep-line.SKW,.dep-line.METRO,.dep-line.S-Bahn{background-color:#008d4f;border-radius:5rem;padding:.2rem .5rem}.dep-line.U,.dep-line.M,.dep-line.SUBWAY,.dep-line.U-Bahn,.dep-line.UBAHN,.dep-line.Schw-B,.dep-line.Schwebebahn,.dep-line.H-Bahn{background-color:#014e8d;border-radius:5rem;padding:.2rem .5rem}.dep-line.RE,.dep-line.IRE,.dep-line.REX,.dep-line.REGIONAL_FAST_RAIL{background-color:#ff4f00}.dep-line.RB,.dep-line.MEX,.dep-line.TER,.dep-line.R,.dep-line.REGIONAL_RAIL,.dep-line.Regionalzug,.dep-line.R-Bahn,.dep-line.BRB{background-color:#1f4a87}.dep-line.IC,.dep-line.ICE,.dep-line.EC,.dep-line.ECE,.dep-line.D,.dep-line.IR,.dep-line.TGV,.dep-line.OGV,.dep-line.EST,.dep-line.TLK,.dep-line.EIC,.dep-line.HIGHSPEED_RAIL,.dep-line.LONG_DISTANCE{background-color:#ff0404;font-weight:900;font-style:italic;padding:.2rem}.dep-line.RJ,.dep-line.RJX{background-color:#c63131}.dep-line.NJ,.dep-line.EN,.dep-line.NIGHT_RAIL{background-color:#29255b}.dep-line.WB{background-color:#2e85ce}.dep-line.FLX{background-color:#71d800;color:black}.departures.connections li{grid-template-columns:15ch 10ch 1fr}.departures.connections .connect-platform-wrapper{text-align:center}.departures.connections .connect-platform-wrapper span{display:block}.status-card-progress-annot{padding-bottom:2ex;border-bottom:2px dashed #808080}.timeline-in-transit .status-card-progress-annot{border-bottom:none}@media screen and (max-width: 600px){.collection.departures li{grid-template-columns:10ch 1fr}.collection.departures li .dep-line,.collection.departures li .dep-time,.collection.departures li .connect-platform-wrapper{grid-column:1;text-align:center}.collection.departures li .dep-dest{grid-column:2;grid-row:1 / span 2}.departures.connections li{grid-template-columns:15ch 1fr}.departures.connections li .connect-platform-wrapper span{display:inline-block}}a.timeline-link{padding-top:1ex;padding-bottom:1ex}.progress{background-color:#424242}.progress>.determinate{background-color:#2196F3}html{background-color:#101010}input[type=email],input[type=text],input[type=password],textarea{color:#fff}
diff --git a/public/static/css/light.min.css b/public/static/css/light.min.css
index 22a7b63..651dc5e 100644
--- a/public/static/css/light.min.css
+++ b/public/static/css/light.min.css
@@ -5,4 +5,4 @@
* Copyright 2014 Alfiana E. Sibuea and other contributors
* Released under the MIT license
* https://github.com/fians/Waves/blob/master/LICENSE
- */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);transition:all 0.7s ease-out;transition-property:transform, opacity;transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none !important}.waves-circle{transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width : 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:0.8rem;transform:translateY(-140%)}.materialboxed{display:block;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#000}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;box-shadow:none;box-sizing:content-box;transition:box-shadow .3s, border .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#4CAF50}input:not([type]):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}.select-wrapper.valid>input.select-dropdown,input.valid:not([type]),input.valid:not([type]):focus,input[type=text].valid:not(.browser-default),input[type=text].valid:not(.browser-default):focus,input[type=password].valid:not(.browser-default),input[type=password].valid:not(.browser-default):focus,input[type=email].valid:not(.browser-default),input[type=email].valid:not(.browser-default):focus,input[type=url].valid:not(.browser-default),input[type=url].valid:not(.browser-default):focus,input[type=time].valid:not(.browser-default),input[type=time].valid:not(.browser-default):focus,input[type=date].valid:not(.browser-default),input[type=date].valid:not(.browser-default):focus,input[type=datetime].valid:not(.browser-default),input[type=datetime].valid:not(.browser-default):focus,input[type=datetime-local].valid:not(.browser-default),input[type=datetime-local].valid:not(.browser-default):focus,input[type=tel].valid:not(.browser-default),input[type=tel].valid:not(.browser-default):focus,input[type=number].valid:not(.browser-default),input[type=number].valid:not(.browser-default):focus,input[type=search].valid:not(.browser-default),input[type=search].valid:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus{border-bottom:1px solid #4CAF50;box-shadow:0 1px 0 0 #4CAF50}.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus,input.invalid:not([type]),input.invalid:not([type]):focus,input[type=text].invalid:not(.browser-default),input[type=text].invalid:not(.browser-default):focus,input[type=password].invalid:not(.browser-default),input[type=password].invalid:not(.browser-default):focus,input[type=email].invalid:not(.browser-default),input[type=email].invalid:not(.browser-default):focus,input[type=url].invalid:not(.browser-default),input[type=url].invalid:not(.browser-default):focus,input[type=time].invalid:not(.browser-default),input[type=time].invalid:not(.browser-default):focus,input[type=date].invalid:not(.browser-default),input[type=date].invalid:not(.browser-default):focus,input[type=datetime].invalid:not(.browser-default),input[type=datetime].invalid:not(.browser-default):focus,input[type=datetime-local].invalid:not(.browser-default),input[type=datetime-local].invalid:not(.browser-default):focus,input[type=tel].invalid:not(.browser-default),input[type=tel].invalid:not(.browser-default):focus,input[type=number].invalid:not(.browser-default),input[type=number].invalid:not(.browser-default):focus,input[type=search].invalid:not(.browser-default),input[type=search].invalid:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus{border-bottom:1px solid #F44336;box-shadow:0 1px 0 0 #F44336}.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid~.helper-text[data-error],input.valid:not([type])~.helper-text[data-success],input.valid:not([type]):focus~.helper-text[data-success],input.invalid:not([type])~.helper-text[data-error],input.invalid:not([type]):focus~.helper-text[data-error],input[type=text].valid:not(.browser-default)~.helper-text[data-success],input[type=text].invalid:not(.browser-default)~.helper-text[data-error],input[type=password].valid:not(.browser-default)~.helper-text[data-success],input[type=password].invalid:not(.browser-default)~.helper-text[data-error],input[type=email].valid:not(.browser-default)~.helper-text[data-success],input[type=email].invalid:not(.browser-default)~.helper-text[data-error],input[type=url].valid:not(.browser-default)~.helper-text[data-success],input[type=url].invalid:not(.browser-default)~.helper-text[data-error],input[type=time].valid:not(.browser-default)~.helper-text[data-success],input[type=time].invalid:not(.browser-default)~.helper-text[data-error],input[type=date].valid:not(.browser-default)~.helper-text[data-success],input[type=date].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime-local].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime-local].invalid:not(.browser-default)~.helper-text[data-error],input[type=tel].valid:not(.browser-default)~.helper-text[data-success],input[type=tel].invalid:not(.browser-default)~.helper-text[data-error],input[type=number].valid:not(.browser-default)~.helper-text[data-success],input[type=number].invalid:not(.browser-default)~.helper-text[data-error],input[type=search].valid:not(.browser-default)~.helper-text[data-success],input[type=search].invalid:not(.browser-default)~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea.valid:focus~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.invalid:focus~.helper-text[data-error]{color:transparent;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input.valid:not([type])~.helper-text:after,input.valid:not([type]):focus~.helper-text:after,input[type=text].valid:not(.browser-default)~.helper-text:after,input[type=text].valid:not(.browser-default):focus~.helper-text:after,input[type=password].valid:not(.browser-default)~.helper-text:after,input[type=password].valid:not(.browser-default):focus~.helper-text:after,input[type=email].valid:not(.browser-default)~.helper-text:after,input[type=email].valid:not(.browser-default):focus~.helper-text:after,input[type=url].valid:not(.browser-default)~.helper-text:after,input[type=url].valid:not(.browser-default):focus~.helper-text:after,input[type=time].valid:not(.browser-default)~.helper-text:after,input[type=time].valid:not(.browser-default):focus~.helper-text:after,input[type=date].valid:not(.browser-default)~.helper-text:after,input[type=date].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime].valid:not(.browser-default)~.helper-text:after,input[type=datetime].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].valid:not(.browser-default)~.helper-text:after,input[type=datetime-local].valid:not(.browser-default):focus~.helper-text:after,input[type=tel].valid:not(.browser-default)~.helper-text:after,input[type=tel].valid:not(.browser-default):focus~.helper-text:after,input[type=number].valid:not(.browser-default)~.helper-text:after,input[type=number].valid:not(.browser-default):focus~.helper-text:after,input[type=search].valid:not(.browser-default)~.helper-text:after,input[type=search].valid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea.valid:focus~.helper-text:after{content:attr(data-success);color:#4CAF50}.select-wrapper.invalid~.helper-text:after,input.invalid:not([type])~.helper-text:after,input.invalid:not([type]):focus~.helper-text:after,input[type=text].invalid:not(.browser-default)~.helper-text:after,input[type=text].invalid:not(.browser-default):focus~.helper-text:after,input[type=password].invalid:not(.browser-default)~.helper-text:after,input[type=password].invalid:not(.browser-default):focus~.helper-text:after,input[type=email].invalid:not(.browser-default)~.helper-text:after,input[type=email].invalid:not(.browser-default):focus~.helper-text:after,input[type=url].invalid:not(.browser-default)~.helper-text:after,input[type=url].invalid:not(.browser-default):focus~.helper-text:after,input[type=time].invalid:not(.browser-default)~.helper-text:after,input[type=time].invalid:not(.browser-default):focus~.helper-text:after,input[type=date].invalid:not(.browser-default)~.helper-text:after,input[type=date].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime].invalid:not(.browser-default)~.helper-text:after,input[type=datetime].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default)~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default):focus~.helper-text:after,input[type=tel].invalid:not(.browser-default)~.helper-text:after,input[type=tel].invalid:not(.browser-default):focus~.helper-text:after,input[type=number].invalid:not(.browser-default)~.helper-text:after,input[type=number].invalid:not(.browser-default):focus~.helper-text:after,input[type=search].invalid:not(.browser-default)~.helper-text:after,input[type=search].invalid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea.invalid:focus~.helper-text:after{content:attr(data-error);color:#F44336}.select-wrapper+label:after,input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~label,.input-field.col .prefix~.validate~label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#000;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:transform .2s ease-out, color .2s ease-out;transform-origin:0% 100%;text-align:initial;transform:translateY(12px)}.input-field>label:not(.label-icon).active{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix~input,.input-field .prefix~textarea,.input-field .prefix~label,.input-field .prefix~.validate~label,.input-field .prefix~.helper-text,.input-field .prefix~.autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width : 992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width : 600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default)~.material-icons{color:#444}.input-field input[type=search]+.label-icon{transform:none;left:1rem}.input-field input[type=search]~.mdi-navigation-close,.input-field input[type=search]~.material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;transform:rotate(40deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;transform:rotate(90deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled)~.lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled)~.lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;transform-origin:50% 50%;transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;backface-visibility:hidden;transform:translateX(-105%)}.sidenav.right-aligned{right:0;transform:translateX(105%);left:auto;transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width : 992px){.sidenav.sidenav-fixed{transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;perspective:500px;transform-style:preserve-3d;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:#616161;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:rgba(0,0,0,0.87)}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{transform:scale(1);opacity:.95;transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;transition:opacity .3s, transform .3s, visibility 0s 1s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;transform:scale(0);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{transform:scale(0);transition:transform .3s}.tap-target-wave::after{visibility:hidden;transition:opacity .3s, transform .3s, visibility 0s;z-index:-1}.tap-target-origin{top:50%;left:50%;transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s, transform .3s;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@keyframes pulse-animation{0%{opacity:1;transform:scale(1)}50%{opacity:0;transform:scale(1.5)}100%{opacity:0;transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.datepicker-controls{display:flex;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width : 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{flex-direction:row}.datepicker-date-display{flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{transition:transform 350ms, opacity 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{transform:scale(0.8, 0.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:flex;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width : 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}.caution-color{background-color:#F44336}.caution-color-text{color:#F44336}.info-color{background-color:#fff9c4}.info-color-text{color:#fff9c4}.contrast-color{background-color:rgba(0,0,0,0.87)}.contrast-color-text{color:rgba(0,0,0,0.87)}.success-color{background-color:#4CAF50}.success-color-text{color:#4CAF50}html{background-color:#fff}a.unmarked{color:rgba(0,0,0,0.87)}.white-text a{color:#eeeeff}div.targetlist{display:grid;grid-template-columns:1fr max-content;align-items:center}div.targetlist>a.nonflex{padding-left:1em;padding-top:1em;padding-bottom:1em;display:inline-block}a.tablerow{display:flex;justify-content:space-between;padding-top:1em;padding-bottom:1em;border-bottom:1px solid rgba(0,0,0,0.12)}a.tablerow .material-icons{vertical-align:bottom;margin-bottom:0.2em}a.tablerow span{display:inline-block}.pagination li a{color:rgba(0,0,0,0.87)}.collection-item{color:rgba(0,0,0,0.87)}.collection-item .secondary-content{color:rgba(0,0,0,0.87)}.collection-item.disabled{color:#616161}.action-checkin,.action-checkout,.action-undo,.action-cancelled-from,.action-cancelled-to,.action-share{cursor:pointer}.config a{cursor:pointer}.navbar-fixed{z-index:1001}.brand-logo span{transition:color 1s}.brand-logo:hover .ca,.brand-logo:hover .ce{color:#a8e3fa !important}.brand-logo:hover .cb,.brand-logo:hover .cd{color:#f5c4ce !important}.wagons span{margin-right:0.5ex;color:#808080}.wagons .wagonclass{font-weight:bold;color:inherit}.wagons .wagonnum{margin-right:0;color:inherit}.wagons .checksum:before{content:"-"}h1{font-size:2.92rem;margin:1.9466666667rem 0 1.168rem 0}h2{font-size:2.28rem;margin:1.52rem 0 .912rem 0}h3{font-size:1.64rem;margin:1.0933333333rem 0 .656rem 0}.geolocation i.material-icons{font-size:16px}ul.suggestions li{padding-top:0.5rem;padding-bottom:0.5rem}.collection.departures>li,.collection.history>li{transition:background .3s;display:grid}.collection.departures>li:not(#now,.history-date-change):hover,.collection.departures>li:focus-within,.collection.history>li:not(#now,.history-date-change):hover,.collection.history>li:focus-within{background-color:#eee;outline:2px solid #039be5}.collection.departures li{grid-template-columns:10ch 10ch 1fr;align-items:center}.collection.departures li#now{background-color:#eee;padding:2rem 20px;grid-template-columns:10ch 1fr}.collection.departures li#now strong{font-weight:bold}.collection.departures li.cancelled{background-color:#FFCDD2;font-style:italic}.collection.departures li.cancelled .dep-line{background-color:transparent;border:1px solid;color:rgba(0,0,0,0.87)}.collection.departures li.cancelled .dep-time::after{content:" ⊖";font-style:normal}.departures .dep-time{color:rgba(0,0,0,0.87)}.departures .dep-time:focus{outline:none}.departures .dep-dest{margin-left:0.8rem}.departures .dep-dest .followee-checkin{font-size:0.9rem;display:block}.departures .dep-dest .followee-checkin i.material-icons{vertical-align:middle}.collection.history>li{display:grid;grid-template-columns:10ch 1fr;grid-template-rows:1fr}.collection.history>li a:first-child{align-self:center;text-align:center;display:flex}.collection.history>li.history-date-change{display:block;font-weight:bold}ul.route-history>li{list-style:none;position:relative;display:grid;grid-template-columns:1rem 1fr;gap:0.5rem}ul.route-history>li a{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul.route-history>li strong{font-weight:600}ul.route-history>li i.material-icons[aria-label=nach]{padding-top:0.4rem}ul.route-history>li i.material-icons[aria-label=von]{display:block;transform:rotate(-90deg);height:1rem;margin-top:0.4rem}ul.route-history>li::before{content:'';background:rgba(0,0,0,0.87);position:absolute;width:2px;left:calc( (1rem - 2px) / 2);bottom:0;top:0}ul.route-history>li:first-of-type::before{top:1.3rem}ul.route-history>li:last-of-type::before{bottom:unset;height:0.5rem}.dep-line{text-align:center;padding:.2rem;color:white;background:#424242;border-radius:.2rem;display:inline-block;font-weight:600;line-height:1;height:fit-content;width:fit-content;min-width:6ch;margin:0 auto}.dep-line.Bus,.dep-line.RUF,.dep-line.AST{background-color:#a3167e;border-radius:5rem;padding:.2rem .5rem}.dep-line.STR{background-color:#c5161c;border-radius:5rem;padding:.2rem .5rem}.dep-line.S,.dep-line.RS,.dep-line.RER,.dep-line.SKW{background-color:#008d4f;border-radius:5rem;padding:.2rem .5rem}.dep-line.U,.dep-line.STB,.dep-line.M{background-color:#014e8d;border-radius:5rem;padding:.2rem .5rem}.dep-line.RE,.dep-line.IRE,.dep-line.REX{background-color:#ff4f00}.dep-line.RB,.dep-line.MEX,.dep-line.TER,.dep-line.R{background-color:#1f4a87}.dep-line.IC,.dep-line.ICE,.dep-line.EC,.dep-line.ECE,.dep-line.D,.dep-line.IR,.dep-line.TGV,.dep-line.OGV,.dep-line.EST,.dep-line.TLK,.dep-line.EIC{background-color:#ff0404;font-weight:900;font-style:italic;padding:.2rem}.dep-line.RJ,.dep-line.RJX{background-color:#c63131}.dep-line.NJ,.dep-line.EN{background-color:#29255b}.dep-line.WB{background-color:#2e85ce}.dep-line.FLX{background-color:#71d800;color:black}.departures.connections li{grid-template-columns:15ch 10ch 1fr}.departures.connections .connect-platform-wrapper{text-align:center}.departures.connections .connect-platform-wrapper span{display:block}@media screen and (max-width: 600px){.collection.departures li{grid-template-columns:10ch 1fr}.collection.departures li .dep-line,.collection.departures li .dep-time,.collection.departures li .connect-platform-wrapper{grid-column:1;text-align:center}.collection.departures li .dep-dest{grid-column:2;grid-row:1 / span 2}.departures.connections li{grid-template-columns:15ch 1fr}.departures.connections li .connect-platform-wrapper span{display:inline-block}}.progress{background-color:#e0e0e0}.progress>.determinate{background-color:#2196F3}
+ */.waves-effect{position:relative;cursor:pointer;display:inline-block;overflow:hidden;user-select:none;-webkit-tap-highlight-color:transparent;vertical-align:middle;z-index:1;transition:.3s ease-out}.waves-effect .waves-ripple{position:absolute;border-radius:50%;width:20px;height:20px;margin-top:-10px;margin-left:-10px;opacity:0;background:rgba(0,0,0,0.2);transition:all 0.7s ease-out;transition-property:transform, opacity;transform:scale(0);pointer-events:none}.waves-effect.waves-light .waves-ripple{background-color:rgba(255,255,255,0.45)}.waves-effect.waves-red .waves-ripple{background-color:rgba(244,67,54,0.7)}.waves-effect.waves-yellow .waves-ripple{background-color:rgba(255,235,59,0.7)}.waves-effect.waves-orange .waves-ripple{background-color:rgba(255,152,0,0.7)}.waves-effect.waves-purple .waves-ripple{background-color:rgba(156,39,176,0.7)}.waves-effect.waves-green .waves-ripple{background-color:rgba(76,175,80,0.7)}.waves-effect.waves-teal .waves-ripple{background-color:rgba(0,150,136,0.7)}.waves-effect input[type="button"],.waves-effect input[type="reset"],.waves-effect input[type="submit"]{border:0;font-style:normal;font-size:inherit;text-transform:inherit;background:none}.waves-effect img{position:relative;z-index:-1}.waves-notransition{transition:none !important}.waves-circle{transform:translateZ(0);-webkit-mask-image:-webkit-radial-gradient(circle, white 100%, black 100%)}.waves-input-wrapper{border-radius:0.2em;vertical-align:bottom}.waves-input-wrapper .waves-button-input{position:relative;top:0;left:0;z-index:1}.waves-circle{text-align:center;width:2.5em;height:2.5em;line-height:2.5em;border-radius:50%;-webkit-mask-image:none}.waves-block{display:block}.waves-effect .waves-ripple{z-index:-1}.modal{display:none;position:fixed;left:0;right:0;background-color:#fafafa;padding:0;max-height:70%;width:55%;margin:auto;overflow-y:auto;border-radius:2px;will-change:top, opacity}.modal:focus{outline:none}@media only screen and (max-width : 992px){.modal{width:80%}}.modal h1,.modal h2,.modal h3,.modal h4{margin-top:0}.modal .modal-content{padding:24px}.modal .modal-close{cursor:pointer}.modal .modal-footer{border-radius:0 0 2px 2px;background-color:#fafafa;padding:4px 6px;height:56px;width:100%;text-align:right}.modal .modal-footer .btn,.modal .modal-footer .btn-large,.modal .modal-footer .btn-small,.modal .modal-footer .btn-flat{margin:6px 0}.modal-overlay{position:fixed;z-index:999;top:-25%;left:0;bottom:0;right:0;height:125%;width:100%;background:#000;display:none;will-change:opacity}.modal.modal-fixed-footer{padding:0;height:70%}.modal.modal-fixed-footer .modal-content{position:absolute;height:calc(100% - 56px);max-height:100%;width:100%;overflow-y:auto}.modal.modal-fixed-footer .modal-footer{border-top:1px solid rgba(0,0,0,0.1);position:absolute;bottom:0}.modal.bottom-sheet{top:auto;bottom:-100%;margin:0;width:100%;max-height:45%;border-radius:0;will-change:bottom, opacity}.collapsible{border-top:1px solid #ddd;border-right:1px solid #ddd;border-left:1px solid #ddd;margin:.5rem 0 1rem 0}.collapsible-header{display:flex;cursor:pointer;-webkit-tap-highlight-color:transparent;line-height:1.5;padding:1rem;background-color:#fff;border-bottom:1px solid #ddd}.collapsible-header:focus{outline:0}.collapsible-header i{width:2rem;font-size:1.6rem;display:inline-block;text-align:center;margin-right:1rem}.keyboard-focused .collapsible-header:focus{background-color:#eee}.collapsible-body{display:none;border-bottom:1px solid #ddd;box-sizing:border-box;padding:2rem}.sidenav .collapsible,.sidenav.fixed .collapsible{border:none;box-shadow:none}.sidenav .collapsible li,.sidenav.fixed .collapsible li{padding:0}.sidenav .collapsible-header,.sidenav.fixed .collapsible-header{background-color:transparent;border:none;line-height:inherit;height:inherit;padding:0 16px}.sidenav .collapsible-header:hover,.sidenav.fixed .collapsible-header:hover{background-color:rgba(0,0,0,0.05)}.sidenav .collapsible-header i,.sidenav.fixed .collapsible-header i{line-height:inherit}.sidenav .collapsible-body,.sidenav.fixed .collapsible-body{border:0;background-color:#fff}.sidenav .collapsible-body li a,.sidenav.fixed .collapsible-body li a{padding:0 23.5px 0 31px}.collapsible.popout{border:none;box-shadow:none}.collapsible.popout>li{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12);margin:0 24px;transition:margin 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94)}.collapsible.popout>li.active{box-shadow:0 5px 11px 0 rgba(0,0,0,0.18),0 4px 15px 0 rgba(0,0,0,0.15);margin:16px 0}.chip{display:inline-block;height:32px;font-size:13px;font-weight:500;color:rgba(0,0,0,0.6);line-height:32px;padding:0 12px;border-radius:16px;background-color:#e4e4e4;margin-bottom:5px;margin-right:5px}.chip:focus{outline:none;background-color:#26a69a;color:#fff}.chip>img{float:left;margin:0 8px 0 -12px;height:32px;width:32px;border-radius:50%}.chip .close{cursor:pointer;float:right;font-size:16px;line-height:32px;padding-left:8px}.chips{border:none;border-bottom:1px solid #9e9e9e;box-shadow:none;margin:0 0 8px 0;min-height:45px;outline:none;transition:all .3s}.chips.focus{border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}.chips:hover{cursor:text}.chips .input{background:none;border:0;color:rgba(0,0,0,0.6);display:inline-block;font-size:16px;height:3rem;line-height:32px;outline:0;margin:0;padding:0 !important;width:120px !important}.chips .input:focus{border:0 !important;box-shadow:none !important}.chips .autocomplete-content{margin-top:0;margin-bottom:0}.prefix~.chips{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.chips:empty~label{font-size:0.8rem;transform:translateY(-140%)}.materialboxed{display:block;cursor:zoom-in;position:relative;transition:opacity .4s;-webkit-backface-visibility:hidden}.materialboxed:hover:not(.active){opacity:.8}.materialboxed.active{cursor:zoom-out}#materialbox-overlay{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#292929;z-index:1000;will-change:opacity}.materialbox-caption{position:fixed;display:none;color:#fff;line-height:50px;bottom:0;left:0;width:100%;text-align:center;padding:0% 15%;height:50px;z-index:1000;-webkit-font-smoothing:antialiased}select:focus{outline:1px solid #c9f3ef}button:focus{outline:none;background-color:#2ab7a9}label{font-size:.8rem;color:#000}::placeholder{color:#d1d1d1}input:not([type]),input[type=text]:not(.browser-default),input[type=password]:not(.browser-default),input[type=email]:not(.browser-default),input[type=url]:not(.browser-default),input[type=time]:not(.browser-default),input[type=date]:not(.browser-default),input[type=datetime]:not(.browser-default),input[type=datetime-local]:not(.browser-default),input[type=tel]:not(.browser-default),input[type=number]:not(.browser-default),input[type=search]:not(.browser-default),textarea.materialize-textarea{background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;border-radius:0;outline:none;height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;box-shadow:none;box-sizing:content-box;transition:box-shadow .3s, border .3s}input:not([type]):disabled,input:not([type])[readonly="readonly"],input[type=text]:not(.browser-default):disabled,input[type=text]:not(.browser-default)[readonly="readonly"],input[type=password]:not(.browser-default):disabled,input[type=password]:not(.browser-default)[readonly="readonly"],input[type=email]:not(.browser-default):disabled,input[type=email]:not(.browser-default)[readonly="readonly"],input[type=url]:not(.browser-default):disabled,input[type=url]:not(.browser-default)[readonly="readonly"],input[type=time]:not(.browser-default):disabled,input[type=time]:not(.browser-default)[readonly="readonly"],input[type=date]:not(.browser-default):disabled,input[type=date]:not(.browser-default)[readonly="readonly"],input[type=datetime]:not(.browser-default):disabled,input[type=datetime]:not(.browser-default)[readonly="readonly"],input[type=datetime-local]:not(.browser-default):disabled,input[type=datetime-local]:not(.browser-default)[readonly="readonly"],input[type=tel]:not(.browser-default):disabled,input[type=tel]:not(.browser-default)[readonly="readonly"],input[type=number]:not(.browser-default):disabled,input[type=number]:not(.browser-default)[readonly="readonly"],input[type=search]:not(.browser-default):disabled,input[type=search]:not(.browser-default)[readonly="readonly"],textarea.materialize-textarea:disabled,textarea.materialize-textarea[readonly="readonly"]{color:rgba(0,0,0,0.42);border-bottom:1px dotted rgba(0,0,0,0.42)}input:not([type]):disabled+label,input:not([type])[readonly="readonly"]+label,input[type=text]:not(.browser-default):disabled+label,input[type=text]:not(.browser-default)[readonly="readonly"]+label,input[type=password]:not(.browser-default):disabled+label,input[type=password]:not(.browser-default)[readonly="readonly"]+label,input[type=email]:not(.browser-default):disabled+label,input[type=email]:not(.browser-default)[readonly="readonly"]+label,input[type=url]:not(.browser-default):disabled+label,input[type=url]:not(.browser-default)[readonly="readonly"]+label,input[type=time]:not(.browser-default):disabled+label,input[type=time]:not(.browser-default)[readonly="readonly"]+label,input[type=date]:not(.browser-default):disabled+label,input[type=date]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime]:not(.browser-default):disabled+label,input[type=datetime]:not(.browser-default)[readonly="readonly"]+label,input[type=datetime-local]:not(.browser-default):disabled+label,input[type=datetime-local]:not(.browser-default)[readonly="readonly"]+label,input[type=tel]:not(.browser-default):disabled+label,input[type=tel]:not(.browser-default)[readonly="readonly"]+label,input[type=number]:not(.browser-default):disabled+label,input[type=number]:not(.browser-default)[readonly="readonly"]+label,input[type=search]:not(.browser-default):disabled+label,input[type=search]:not(.browser-default)[readonly="readonly"]+label,textarea.materialize-textarea:disabled+label,textarea.materialize-textarea[readonly="readonly"]+label{color:rgba(0,0,0,0.42)}input:not([type]):focus:not([readonly]),input[type=text]:not(.browser-default):focus:not([readonly]),input[type=password]:not(.browser-default):focus:not([readonly]),input[type=email]:not(.browser-default):focus:not([readonly]),input[type=url]:not(.browser-default):focus:not([readonly]),input[type=time]:not(.browser-default):focus:not([readonly]),input[type=date]:not(.browser-default):focus:not([readonly]),input[type=datetime]:not(.browser-default):focus:not([readonly]),input[type=datetime-local]:not(.browser-default):focus:not([readonly]),input[type=tel]:not(.browser-default):focus:not([readonly]),input[type=number]:not(.browser-default):focus:not([readonly]),input[type=search]:not(.browser-default):focus:not([readonly]),textarea.materialize-textarea:focus:not([readonly]){border-bottom:1px solid #26a69a;box-shadow:0 1px 0 0 #26a69a}input:not([type]):focus:not([readonly])+label,input[type=text]:not(.browser-default):focus:not([readonly])+label,input[type=password]:not(.browser-default):focus:not([readonly])+label,input[type=email]:not(.browser-default):focus:not([readonly])+label,input[type=url]:not(.browser-default):focus:not([readonly])+label,input[type=time]:not(.browser-default):focus:not([readonly])+label,input[type=date]:not(.browser-default):focus:not([readonly])+label,input[type=datetime]:not(.browser-default):focus:not([readonly])+label,input[type=datetime-local]:not(.browser-default):focus:not([readonly])+label,input[type=tel]:not(.browser-default):focus:not([readonly])+label,input[type=number]:not(.browser-default):focus:not([readonly])+label,input[type=search]:not(.browser-default):focus:not([readonly])+label,textarea.materialize-textarea:focus:not([readonly])+label{color:#26a69a}input:not([type]):focus.valid~label,input[type=text]:not(.browser-default):focus.valid~label,input[type=password]:not(.browser-default):focus.valid~label,input[type=email]:not(.browser-default):focus.valid~label,input[type=url]:not(.browser-default):focus.valid~label,input[type=time]:not(.browser-default):focus.valid~label,input[type=date]:not(.browser-default):focus.valid~label,input[type=datetime]:not(.browser-default):focus.valid~label,input[type=datetime-local]:not(.browser-default):focus.valid~label,input[type=tel]:not(.browser-default):focus.valid~label,input[type=number]:not(.browser-default):focus.valid~label,input[type=search]:not(.browser-default):focus.valid~label,textarea.materialize-textarea:focus.valid~label{color:#4CAF50}input:not([type]):focus.invalid~label,input[type=text]:not(.browser-default):focus.invalid~label,input[type=password]:not(.browser-default):focus.invalid~label,input[type=email]:not(.browser-default):focus.invalid~label,input[type=url]:not(.browser-default):focus.invalid~label,input[type=time]:not(.browser-default):focus.invalid~label,input[type=date]:not(.browser-default):focus.invalid~label,input[type=datetime]:not(.browser-default):focus.invalid~label,input[type=datetime-local]:not(.browser-default):focus.invalid~label,input[type=tel]:not(.browser-default):focus.invalid~label,input[type=number]:not(.browser-default):focus.invalid~label,input[type=search]:not(.browser-default):focus.invalid~label,textarea.materialize-textarea:focus.invalid~label{color:#F44336}input:not([type]).validate+label,input[type=text]:not(.browser-default).validate+label,input[type=password]:not(.browser-default).validate+label,input[type=email]:not(.browser-default).validate+label,input[type=url]:not(.browser-default).validate+label,input[type=time]:not(.browser-default).validate+label,input[type=date]:not(.browser-default).validate+label,input[type=datetime]:not(.browser-default).validate+label,input[type=datetime-local]:not(.browser-default).validate+label,input[type=tel]:not(.browser-default).validate+label,input[type=number]:not(.browser-default).validate+label,input[type=search]:not(.browser-default).validate+label,textarea.materialize-textarea.validate+label{width:100%}.select-wrapper.valid>input.select-dropdown,input.valid:not([type]),input.valid:not([type]):focus,input[type=text].valid:not(.browser-default),input[type=text].valid:not(.browser-default):focus,input[type=password].valid:not(.browser-default),input[type=password].valid:not(.browser-default):focus,input[type=email].valid:not(.browser-default),input[type=email].valid:not(.browser-default):focus,input[type=url].valid:not(.browser-default),input[type=url].valid:not(.browser-default):focus,input[type=time].valid:not(.browser-default),input[type=time].valid:not(.browser-default):focus,input[type=date].valid:not(.browser-default),input[type=date].valid:not(.browser-default):focus,input[type=datetime].valid:not(.browser-default),input[type=datetime].valid:not(.browser-default):focus,input[type=datetime-local].valid:not(.browser-default),input[type=datetime-local].valid:not(.browser-default):focus,input[type=tel].valid:not(.browser-default),input[type=tel].valid:not(.browser-default):focus,input[type=number].valid:not(.browser-default),input[type=number].valid:not(.browser-default):focus,input[type=search].valid:not(.browser-default),input[type=search].valid:not(.browser-default):focus,textarea.materialize-textarea.valid,textarea.materialize-textarea.valid:focus{border-bottom:1px solid #4CAF50;box-shadow:0 1px 0 0 #4CAF50}.select-wrapper.invalid>input.select-dropdown,.select-wrapper.invalid>input.select-dropdown:focus,input.invalid:not([type]),input.invalid:not([type]):focus,input[type=text].invalid:not(.browser-default),input[type=text].invalid:not(.browser-default):focus,input[type=password].invalid:not(.browser-default),input[type=password].invalid:not(.browser-default):focus,input[type=email].invalid:not(.browser-default),input[type=email].invalid:not(.browser-default):focus,input[type=url].invalid:not(.browser-default),input[type=url].invalid:not(.browser-default):focus,input[type=time].invalid:not(.browser-default),input[type=time].invalid:not(.browser-default):focus,input[type=date].invalid:not(.browser-default),input[type=date].invalid:not(.browser-default):focus,input[type=datetime].invalid:not(.browser-default),input[type=datetime].invalid:not(.browser-default):focus,input[type=datetime-local].invalid:not(.browser-default),input[type=datetime-local].invalid:not(.browser-default):focus,input[type=tel].invalid:not(.browser-default),input[type=tel].invalid:not(.browser-default):focus,input[type=number].invalid:not(.browser-default),input[type=number].invalid:not(.browser-default):focus,input[type=search].invalid:not(.browser-default),input[type=search].invalid:not(.browser-default):focus,textarea.materialize-textarea.invalid,textarea.materialize-textarea.invalid:focus{border-bottom:1px solid #F44336;box-shadow:0 1px 0 0 #F44336}.select-wrapper.valid .helper-text[data-success],.select-wrapper.invalid~.helper-text[data-error],input.valid:not([type])~.helper-text[data-success],input.valid:not([type]):focus~.helper-text[data-success],input.invalid:not([type])~.helper-text[data-error],input.invalid:not([type]):focus~.helper-text[data-error],input[type=text].valid:not(.browser-default)~.helper-text[data-success],input[type=text].invalid:not(.browser-default)~.helper-text[data-error],input[type=password].valid:not(.browser-default)~.helper-text[data-success],input[type=password].invalid:not(.browser-default)~.helper-text[data-error],input[type=email].valid:not(.browser-default)~.helper-text[data-success],input[type=email].invalid:not(.browser-default)~.helper-text[data-error],input[type=url].valid:not(.browser-default)~.helper-text[data-success],input[type=url].invalid:not(.browser-default)~.helper-text[data-error],input[type=time].valid:not(.browser-default)~.helper-text[data-success],input[type=time].invalid:not(.browser-default)~.helper-text[data-error],input[type=date].valid:not(.browser-default)~.helper-text[data-success],input[type=date].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime].invalid:not(.browser-default)~.helper-text[data-error],input[type=datetime-local].valid:not(.browser-default)~.helper-text[data-success],input[type=datetime-local].invalid:not(.browser-default)~.helper-text[data-error],input[type=tel].valid:not(.browser-default)~.helper-text[data-success],input[type=tel].invalid:not(.browser-default)~.helper-text[data-error],input[type=number].valid:not(.browser-default)~.helper-text[data-success],input[type=number].invalid:not(.browser-default)~.helper-text[data-error],input[type=search].valid:not(.browser-default)~.helper-text[data-success],input[type=search].invalid:not(.browser-default)~.helper-text[data-error],textarea.materialize-textarea.valid~.helper-text[data-success],textarea.materialize-textarea.valid:focus~.helper-text[data-success],textarea.materialize-textarea.invalid~.helper-text[data-error],textarea.materialize-textarea.invalid:focus~.helper-text[data-error]{color:transparent;user-select:none;pointer-events:none}.select-wrapper.valid~.helper-text:after,input.valid:not([type])~.helper-text:after,input.valid:not([type]):focus~.helper-text:after,input[type=text].valid:not(.browser-default)~.helper-text:after,input[type=text].valid:not(.browser-default):focus~.helper-text:after,input[type=password].valid:not(.browser-default)~.helper-text:after,input[type=password].valid:not(.browser-default):focus~.helper-text:after,input[type=email].valid:not(.browser-default)~.helper-text:after,input[type=email].valid:not(.browser-default):focus~.helper-text:after,input[type=url].valid:not(.browser-default)~.helper-text:after,input[type=url].valid:not(.browser-default):focus~.helper-text:after,input[type=time].valid:not(.browser-default)~.helper-text:after,input[type=time].valid:not(.browser-default):focus~.helper-text:after,input[type=date].valid:not(.browser-default)~.helper-text:after,input[type=date].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime].valid:not(.browser-default)~.helper-text:after,input[type=datetime].valid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].valid:not(.browser-default)~.helper-text:after,input[type=datetime-local].valid:not(.browser-default):focus~.helper-text:after,input[type=tel].valid:not(.browser-default)~.helper-text:after,input[type=tel].valid:not(.browser-default):focus~.helper-text:after,input[type=number].valid:not(.browser-default)~.helper-text:after,input[type=number].valid:not(.browser-default):focus~.helper-text:after,input[type=search].valid:not(.browser-default)~.helper-text:after,input[type=search].valid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.valid~.helper-text:after,textarea.materialize-textarea.valid:focus~.helper-text:after{content:attr(data-success);color:#4CAF50}.select-wrapper.invalid~.helper-text:after,input.invalid:not([type])~.helper-text:after,input.invalid:not([type]):focus~.helper-text:after,input[type=text].invalid:not(.browser-default)~.helper-text:after,input[type=text].invalid:not(.browser-default):focus~.helper-text:after,input[type=password].invalid:not(.browser-default)~.helper-text:after,input[type=password].invalid:not(.browser-default):focus~.helper-text:after,input[type=email].invalid:not(.browser-default)~.helper-text:after,input[type=email].invalid:not(.browser-default):focus~.helper-text:after,input[type=url].invalid:not(.browser-default)~.helper-text:after,input[type=url].invalid:not(.browser-default):focus~.helper-text:after,input[type=time].invalid:not(.browser-default)~.helper-text:after,input[type=time].invalid:not(.browser-default):focus~.helper-text:after,input[type=date].invalid:not(.browser-default)~.helper-text:after,input[type=date].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime].invalid:not(.browser-default)~.helper-text:after,input[type=datetime].invalid:not(.browser-default):focus~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default)~.helper-text:after,input[type=datetime-local].invalid:not(.browser-default):focus~.helper-text:after,input[type=tel].invalid:not(.browser-default)~.helper-text:after,input[type=tel].invalid:not(.browser-default):focus~.helper-text:after,input[type=number].invalid:not(.browser-default)~.helper-text:after,input[type=number].invalid:not(.browser-default):focus~.helper-text:after,input[type=search].invalid:not(.browser-default)~.helper-text:after,input[type=search].invalid:not(.browser-default):focus~.helper-text:after,textarea.materialize-textarea.invalid~.helper-text:after,textarea.materialize-textarea.invalid:focus~.helper-text:after{content:attr(data-error);color:#F44336}.select-wrapper+label:after,input:not([type])+label:after,input[type=text]:not(.browser-default)+label:after,input[type=password]:not(.browser-default)+label:after,input[type=email]:not(.browser-default)+label:after,input[type=url]:not(.browser-default)+label:after,input[type=time]:not(.browser-default)+label:after,input[type=date]:not(.browser-default)+label:after,input[type=datetime]:not(.browser-default)+label:after,input[type=datetime-local]:not(.browser-default)+label:after,input[type=tel]:not(.browser-default)+label:after,input[type=number]:not(.browser-default)+label:after,input[type=search]:not(.browser-default)+label:after,textarea.materialize-textarea+label:after{display:block;content:"";position:absolute;top:100%;left:0;opacity:0;transition:.2s opacity ease-out, .2s color ease-out}.input-field{position:relative;margin-top:1rem;margin-bottom:1rem}.input-field.inline{display:inline-block;vertical-align:middle;margin-left:5px}.input-field.inline input,.input-field.inline .select-dropdown{margin-bottom:1rem}.input-field.col label{left:.75rem}.input-field.col .prefix~label,.input-field.col .prefix~.validate~label{width:calc(100% - 3rem - 1.5rem)}.input-field>label{color:#000;position:absolute;top:0;left:0;font-size:1rem;cursor:text;transition:transform .2s ease-out, color .2s ease-out;transform-origin:0% 100%;text-align:initial;transform:translateY(12px)}.input-field>label:not(.label-icon).active{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field>input[type]:-webkit-autofill:not(.browser-default):not([type="search"])+label,.input-field>input[type=date]:not(.browser-default)+label,.input-field>input[type=time]:not(.browser-default)+label{transform:translateY(-14px) scale(0.8);transform-origin:0 0}.input-field .helper-text{position:relative;min-height:18px;display:block;font-size:12px;color:rgba(0,0,0,0.54)}.input-field .helper-text::after{opacity:1;position:absolute;top:0;left:0}.input-field .prefix{position:absolute;width:3rem;font-size:2rem;transition:color .2s;top:.5rem}.input-field .prefix.active{color:#26a69a}.input-field .prefix~input,.input-field .prefix~textarea,.input-field .prefix~label,.input-field .prefix~.validate~label,.input-field .prefix~.helper-text,.input-field .prefix~.autocomplete-content{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.input-field .prefix~label{margin-left:3rem}@media only screen and (max-width : 992px){.input-field .prefix~input{width:86%;width:calc(100% - 3rem)}}@media only screen and (max-width : 600px){.input-field .prefix~input{width:80%;width:calc(100% - 3rem)}}.input-field input[type=search]{display:block;line-height:inherit;transition:.3s background-color}.nav-wrapper .input-field input[type=search]{height:inherit;padding-left:4rem;width:calc(100% - 4rem);border:0;box-shadow:none}.input-field input[type=search]:focus:not(.browser-default){background-color:#fff;border:0;box-shadow:none;color:#444}.input-field input[type=search]:focus:not(.browser-default)+label i,.input-field input[type=search]:focus:not(.browser-default)~.mdi-navigation-close,.input-field input[type=search]:focus:not(.browser-default)~.material-icons{color:#444}.input-field input[type=search]+.label-icon{transform:none;left:1rem}.input-field input[type=search]~.mdi-navigation-close,.input-field input[type=search]~.material-icons{position:absolute;top:0;right:1rem;color:transparent;cursor:pointer;font-size:2rem;transition:.3s color}textarea{width:100%;height:3rem;background-color:transparent}textarea.materialize-textarea{line-height:normal;overflow-y:hidden;padding:.8rem 0 .8rem 0;resize:none;min-height:3rem;box-sizing:border-box}.hiddendiv{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;padding-top:1.2rem;position:absolute;top:0;z-index:-1}.autocomplete-content li .highlight{color:#444}.autocomplete-content li img{height:40px;width:40px;margin:5px 15px}.character-counter{min-height:18px}[type="radio"]:not(:checked),[type="radio"]:checked{position:absolute;opacity:0;pointer-events:none}[type="radio"]:not(:checked)+span,[type="radio"]:checked+span{position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;transition:.28s ease;user-select:none}[type="radio"]+span:before,[type="radio"]+span:after{content:'';position:absolute;left:0;top:0;margin:4px;width:16px;height:16px;z-index:0;transition:.28s ease}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after,[type="radio"]:checked+span:before,[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border-radius:50%}[type="radio"]:not(:checked)+span:before,[type="radio"]:not(:checked)+span:after{border:2px solid #5a5a5a}[type="radio"]:not(:checked)+span:after{transform:scale(0)}[type="radio"]:checked+span:before{border:2px solid transparent}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:before,[type="radio"].with-gap:checked+span:after{border:2px solid #26a69a}[type="radio"]:checked+span:after,[type="radio"].with-gap:checked+span:after{background-color:#26a69a}[type="radio"]:checked+span:after{transform:scale(1.02)}[type="radio"].with-gap:checked+span:after{transform:scale(0.5)}[type="radio"].tabbed:focus+span:before{box-shadow:0 0 0 10px rgba(0,0,0,0.1)}[type="radio"].with-gap:disabled:checked+span:before{border:2px solid rgba(0,0,0,0.42)}[type="radio"].with-gap:disabled:checked+span:after{border:none;background-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before,[type="radio"]:disabled:checked+span:before{background-color:transparent;border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled+span{color:rgba(0,0,0,0.42)}[type="radio"]:disabled:not(:checked)+span:before{border-color:rgba(0,0,0,0.42)}[type="radio"]:disabled:checked+span:after{background-color:rgba(0,0,0,0.42);border-color:#949494}[type="checkbox"]:not(:checked),[type="checkbox"]:checked{position:absolute;opacity:0;pointer-events:none}[type="checkbox"]+span:not(.lever){position:relative;padding-left:35px;cursor:pointer;display:inline-block;height:25px;line-height:25px;font-size:1rem;user-select:none}[type="checkbox"]+span:not(.lever):before,[type="checkbox"]:not(.filled-in)+span:not(.lever):after{content:'';position:absolute;top:0;left:0;width:18px;height:18px;z-index:0;border:2px solid #5a5a5a;border-radius:1px;margin-top:3px;transition:.2s}[type="checkbox"]:not(.filled-in)+span:not(.lever):after{border:0;transform:scale(0)}[type="checkbox"]:not(:checked):disabled+span:not(.lever):before{border:none;background-color:rgba(0,0,0,0.42)}[type="checkbox"].tabbed:focus+span:not(.lever):after{transform:scale(1);border:0;border-radius:50%;box-shadow:0 0 0 10px rgba(0,0,0,0.1);background-color:rgba(0,0,0,0.1)}[type="checkbox"]:checked+span:not(.lever):before{top:-4px;left:-5px;width:12px;height:22px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #26a69a;border-bottom:2px solid #26a69a;transform:rotate(40deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:checked:disabled+span:before{border-right:2px solid rgba(0,0,0,0.42);border-bottom:2px solid rgba(0,0,0,0.42)}[type="checkbox"]:indeterminate+span:not(.lever):before{top:-11px;left:-12px;width:10px;height:22px;border-top:none;border-left:none;border-right:2px solid #26a69a;border-bottom:none;transform:rotate(90deg);backface-visibility:hidden;transform-origin:100% 100%}[type="checkbox"]:indeterminate:disabled+span:not(.lever):before{border-right:2px solid rgba(0,0,0,0.42);background-color:transparent}[type="checkbox"].filled-in+span:not(.lever):after{border-radius:2px}[type="checkbox"].filled-in+span:not(.lever):before,[type="checkbox"].filled-in+span:not(.lever):after{content:'';left:0;position:absolute;transition:border .25s, background-color .25s, width .20s .1s, height .20s .1s, top .20s .1s, left .20s .1s;z-index:1}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):before{width:0;height:0;border:3px solid transparent;left:6px;top:10px;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:not(:checked)+span:not(.lever):after{height:20px;width:20px;background-color:transparent;border:2px solid #5a5a5a;top:0px;z-index:0}[type="checkbox"].filled-in:checked+span:not(.lever):before{top:0;left:1px;width:8px;height:13px;border-top:2px solid transparent;border-left:2px solid transparent;border-right:2px solid #fff;border-bottom:2px solid #fff;transform:rotateZ(37deg);transform-origin:100% 100%}[type="checkbox"].filled-in:checked+span:not(.lever):after{top:0;width:20px;height:20px;border:2px solid #26a69a;background-color:#26a69a;z-index:0}[type="checkbox"].filled-in.tabbed:focus+span:not(.lever):after{border-radius:2px;border-color:#5a5a5a;background-color:rgba(0,0,0,0.1)}[type="checkbox"].filled-in.tabbed:checked:focus+span:not(.lever):after{border-radius:2px;background-color:#26a69a;border-color:#26a69a}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):before{background-color:transparent;border:2px solid transparent}[type="checkbox"].filled-in:disabled:not(:checked)+span:not(.lever):after{border-color:transparent;background-color:#949494}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):before{background-color:transparent}[type="checkbox"].filled-in:disabled:checked+span:not(.lever):after{background-color:#949494;border-color:#949494}.switch,.switch *{-webkit-tap-highlight-color:transparent;user-select:none}.switch label{cursor:pointer}.switch label input[type=checkbox]{opacity:0;width:0;height:0}.switch label input[type=checkbox]:checked+.lever{background-color:#84c7c1}.switch label input[type=checkbox]:checked+.lever:before,.switch label input[type=checkbox]:checked+.lever:after{left:18px}.switch label input[type=checkbox]:checked+.lever:after{background-color:#26a69a}.switch label .lever{content:"";display:inline-block;position:relative;width:36px;height:14px;background-color:rgba(0,0,0,0.38);border-radius:15px;margin-right:10px;transition:background 0.3s ease;vertical-align:middle;margin:0 16px}.switch label .lever:before,.switch label .lever:after{content:"";position:absolute;display:inline-block;width:20px;height:20px;border-radius:50%;left:0;top:-3px;transition:left 0.3s ease, background .3s ease, box-shadow 0.1s ease, transform .1s ease}.switch label .lever:before{background-color:rgba(38,166,154,0.15)}.switch label .lever:after{background-color:#F1F1F1;box-shadow:0px 3px 1px -2px rgba(0,0,0,0.2),0px 2px 2px 0px rgba(0,0,0,0.14),0px 1px 5px 0px rgba(0,0,0,0.12)}input[type=checkbox]:checked:not(:disabled)~.lever:active::before,input[type=checkbox]:checked:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(38,166,154,0.15)}input[type=checkbox]:not(:disabled)~.lever:active:before,input[type=checkbox]:not(:disabled).tabbed:focus~.lever::before{transform:scale(2.4);background-color:rgba(0,0,0,0.08)}.switch input[type=checkbox][disabled]+.lever{cursor:default;background-color:rgba(0,0,0,0.12)}.switch label input[type=checkbox][disabled]+.lever:after,.switch label input[type=checkbox][disabled]:checked+.lever:after{background-color:#949494}select{display:none}select.browser-default{display:block}select{background-color:rgba(255,255,255,0.9);width:100%;padding:5px;border:1px solid #f2f2f2;border-radius:2px;height:3rem}.select-label{position:absolute}.select-wrapper{position:relative}.select-wrapper.valid+label,.select-wrapper.invalid+label{width:100%;pointer-events:none}.select-wrapper input.select-dropdown{position:relative;cursor:pointer;background-color:transparent;border:none;border-bottom:1px solid #9e9e9e;outline:none;height:3rem;line-height:3rem;width:100%;font-size:16px;margin:0 0 8px 0;padding:0;display:block;user-select:none;z-index:1}.select-wrapper input.select-dropdown:focus{border-bottom:1px solid #26a69a}.select-wrapper .caret{position:absolute;right:0;top:0;bottom:0;margin:auto 0;z-index:0;fill:rgba(0,0,0,0.87)}.select-wrapper+label{position:absolute;top:-26px;font-size:.8rem}select:disabled{color:rgba(0,0,0,0.42)}.select-wrapper.disabled+label{color:rgba(0,0,0,0.42)}.select-wrapper.disabled .caret{fill:rgba(0,0,0,0.42)}.select-wrapper input.select-dropdown:disabled{color:rgba(0,0,0,0.42);cursor:default;user-select:none}.select-wrapper i{color:rgba(0,0,0,0.3)}.select-dropdown li.disabled,.select-dropdown li.disabled>span,.select-dropdown li.optgroup{color:rgba(0,0,0,0.3);background-color:transparent}body.keyboard-focused .select-dropdown.dropdown-content li:focus{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li:hover{background-color:rgba(0,0,0,0.08)}.select-dropdown.dropdown-content li.selected{background-color:rgba(0,0,0,0.03)}.prefix~.select-wrapper{margin-left:3rem;width:92%;width:calc(100% - 3rem)}.prefix~label{margin-left:3rem}.select-dropdown li img{height:40px;width:40px;margin:5px 15px;float:right}.select-dropdown li.optgroup{border-top:1px solid #eee}.select-dropdown li.optgroup.selected>span{color:rgba(0,0,0,0.7)}.select-dropdown li.optgroup>span{color:rgba(0,0,0,0.4)}.select-dropdown li.optgroup~li.optgroup-option{padding-left:1rem}.file-field{position:relative}.file-field .file-path-wrapper{overflow:hidden;padding-left:10px}.file-field input.file-path{width:100%}.file-field .btn,.file-field .btn-large,.file-field .btn-small{float:left;height:3rem;line-height:3rem}.file-field span{cursor:pointer}.file-field input[type=file]{position:absolute;top:0;right:0;left:0;bottom:0;width:100%;margin:0;padding:0;font-size:20px;cursor:pointer;opacity:0;filter:alpha(opacity=0)}.file-field input[type=file]::-webkit-file-upload-button{display:none}.range-field{position:relative}input[type=range],input[type=range]+.thumb{cursor:pointer}input[type=range]{position:relative;background-color:transparent;border:none;outline:none;width:100%;margin:15px 0;padding:0}input[type=range]:focus{outline:none}input[type=range]+.thumb{position:absolute;top:10px;left:0;border:none;height:0;width:0;border-radius:50%;background-color:#26a69a;margin-left:7px;transform-origin:50% 50%;transform:rotate(-45deg)}input[type=range]+.thumb .value{display:block;width:30px;text-align:center;color:#26a69a;font-size:0;transform:rotate(45deg)}input[type=range]+.thumb.active{border-radius:50% 50% 50% 0}input[type=range]+.thumb.active .value{color:#fff;margin-left:-1px;margin-top:8px;font-size:10px}input[type=range]{-webkit-appearance:none}input[type=range]::-webkit-slider-runnable-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-webkit-slider-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s;-webkit-appearance:none;background-color:#26a69a;transform-origin:50% 50%;margin:-5px 0 0 0}.keyboard-focused input[type=range]:focus:not(.active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]{border:1px solid white}input[type=range]::-moz-range-track{height:3px;background:#c2c0c2;border:none}input[type=range]::-moz-focus-inner{border:0}input[type=range]::-moz-range-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s;margin-top:-5px}input[type=range]:-moz-focusring{outline:1px solid #fff;outline-offset:-1px}.keyboard-focused input[type=range]:focus:not(.active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}input[type=range]::-ms-track{height:3px;background:transparent;border-color:transparent;border-width:6px 0;color:transparent}input[type=range]::-ms-fill-lower{background:#777}input[type=range]::-ms-fill-upper{background:#ddd}input[type=range]::-ms-thumb{border:none;height:14px;width:14px;border-radius:50%;background:#26a69a;transition:box-shadow .3s}.keyboard-focused input[type=range]:focus:not(.active)::-ms-thumb{box-shadow:0 0 0 10px rgba(38,166,154,0.26)}.table-of-contents.fixed{position:fixed}.table-of-contents li{padding:2px 0}.table-of-contents a{display:inline-block;font-weight:300;color:#757575;padding-left:16px;height:1.5rem;line-height:1.5rem;letter-spacing:.4;display:inline-block}.table-of-contents a:hover{color:#a8a8a8;padding-left:15px;border-left:1px solid #ee6e73}.table-of-contents a.active{font-weight:500;padding-left:14px;border-left:2px solid #ee6e73}.sidenav{position:fixed;width:300px;left:0;top:0;margin:0;transform:translateX(-100%);height:100%;height:calc(100% + 60px);height:-moz-calc(100%);padding-bottom:60px;background-color:#fff;z-index:999;overflow-y:auto;will-change:transform;backface-visibility:hidden;transform:translateX(-105%)}.sidenav.right-aligned{right:0;transform:translateX(105%);left:auto;transform:translateX(100%)}.sidenav .collapsible{margin:0}.sidenav li{float:none;line-height:48px}.sidenav li.active{background-color:rgba(0,0,0,0.05)}.sidenav li>a{color:rgba(0,0,0,0.87);display:block;font-size:14px;font-weight:500;height:48px;line-height:48px;padding:0 32px}.sidenav li>a:hover{background-color:rgba(0,0,0,0.05)}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-flat,.sidenav li>a.btn-floating{margin:10px 15px}.sidenav li>a.btn,.sidenav li>a.btn-small,.sidenav li>a.btn-large,.sidenav li>a.btn-floating{color:#fff}.sidenav li>a.btn-flat{color:#343434}.sidenav li>a.btn:hover,.sidenav li>a.btn-small:hover,.sidenav li>a.btn-large:hover{background-color:#2bbbad}.sidenav li>a.btn-floating:hover{background-color:#26a69a}.sidenav li>a>i,.sidenav li>a>[class^="mdi-"],.sidenav li>a li>a>[class*="mdi-"],.sidenav li>a>i.material-icons{float:left;height:48px;line-height:48px;margin:0 32px 0 0;width:24px;color:rgba(0,0,0,0.54)}.sidenav .divider{margin:8px 0 0 0}.sidenav .subheader{cursor:initial;pointer-events:none;color:rgba(0,0,0,0.54);font-size:14px;font-weight:500;line-height:48px}.sidenav .subheader:hover{background-color:transparent}.sidenav .user-view{position:relative;padding:32px 32px 0;margin-bottom:8px}.sidenav .user-view>a{height:auto;padding:0}.sidenav .user-view>a:hover{background-color:transparent}.sidenav .user-view .background{overflow:hidden;position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1}.sidenav .user-view .circle,.sidenav .user-view .name,.sidenav .user-view .email{display:block}.sidenav .user-view .circle{height:64px;width:64px}.sidenav .user-view .name,.sidenav .user-view .email{font-size:14px;line-height:24px}.sidenav .user-view .name{margin-top:16px;font-weight:500}.sidenav .user-view .email{padding-bottom:16px;font-weight:400}.drag-target{height:100%;width:10px;position:fixed;top:0;z-index:998}.drag-target.right-aligned{right:0}.sidenav.sidenav-fixed{left:0;transform:translateX(0);position:fixed}.sidenav.sidenav-fixed.right-aligned{right:0;left:auto}@media only screen and (max-width : 992px){.sidenav.sidenav-fixed{transform:translateX(-105%)}.sidenav.sidenav-fixed.right-aligned{transform:translateX(105%)}.sidenav>a{padding:0 16px}.sidenav .user-view{padding:16px 16px 0}}.sidenav .collapsible-body>ul:not(.collapsible)>li.active,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active{background-color:#ee6e73}.sidenav .collapsible-body>ul:not(.collapsible)>li.active a,.sidenav.sidenav-fixed .collapsible-body>ul:not(.collapsible)>li.active a{color:#fff}.sidenav .collapsible-body{padding:0}.sidenav-overlay{position:fixed;top:0;left:0;right:0;opacity:0;height:120vh;background-color:rgba(0,0,0,0.5);z-index:997;display:none}.preloader-wrapper{display:inline-block;position:relative;width:50px;height:50px}.preloader-wrapper.small{width:36px;height:36px}.preloader-wrapper.big{width:64px;height:64px}.preloader-wrapper.active{-webkit-animation:container-rotate 1568ms linear infinite;animation:container-rotate 1568ms linear infinite}@-webkit-keyframes container-rotate{to{-webkit-transform:rotate(360deg)}}@keyframes container-rotate{to{transform:rotate(360deg)}}.spinner-layer{position:absolute;width:100%;height:100%;opacity:0;border-color:#26a69a}.spinner-blue,.spinner-blue-only{border-color:#4285f4}.spinner-red,.spinner-red-only{border-color:#db4437}.spinner-yellow,.spinner-yellow-only{border-color:#f4b400}.spinner-green,.spinner-green-only{border-color:#0f9d58}.active .spinner-layer.spinner-blue{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,blue-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-red{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,red-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-yellow{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,yellow-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer.spinner-green{-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both,green-fade-in-out 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .spinner-layer,.active .spinner-layer.spinner-blue-only,.active .spinner-layer.spinner-red-only,.active .spinner-layer.spinner-yellow-only,.active .spinner-layer.spinner-green-only{opacity:1;-webkit-animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:fill-unfill-rotate 5332ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg)}}@keyframes fill-unfill-rotate{12.5%{transform:rotate(135deg)}25%{transform:rotate(270deg)}37.5%{transform:rotate(405deg)}50%{transform:rotate(540deg)}62.5%{transform:rotate(675deg)}75%{transform:rotate(810deg)}87.5%{transform:rotate(945deg)}to{transform:rotate(1080deg)}}@-webkit-keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@keyframes blue-fade-in-out{from{opacity:1}25%{opacity:1}26%{opacity:0}89%{opacity:0}90%{opacity:1}100%{opacity:1}}@-webkit-keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@keyframes red-fade-in-out{from{opacity:0}15%{opacity:0}25%{opacity:1}50%{opacity:1}51%{opacity:0}}@-webkit-keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@keyframes yellow-fade-in-out{from{opacity:0}40%{opacity:0}50%{opacity:1}75%{opacity:1}76%{opacity:0}}@-webkit-keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}@keyframes green-fade-in-out{from{opacity:0}65%{opacity:0}75%{opacity:1}90%{opacity:1}100%{opacity:0}}.gap-patch{position:absolute;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.gap-patch .circle{width:1000%;left:-450%}.circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.circle-clipper .circle{width:200%;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent !important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0}.circle-clipper.left .circle{left:0;border-right-color:transparent !important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.circle-clipper.right .circle{left:-100%;border-left-color:transparent !important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.active .circle-clipper.left .circle{-webkit-animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:left-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}.active .circle-clipper.right .circle{-webkit-animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both;animation:right-spin 1333ms cubic-bezier(0.4, 0, 0.2, 1) infinite both}@-webkit-keyframes left-spin{from{-webkit-transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg)}}@keyframes left-spin{from{transform:rotate(130deg)}50%{transform:rotate(-5deg)}to{transform:rotate(130deg)}}@-webkit-keyframes right-spin{from{-webkit-transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg)}}@keyframes right-spin{from{transform:rotate(-130deg)}50%{transform:rotate(5deg)}to{transform:rotate(-130deg)}}#spinnerContainer.cooldown{-webkit-animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1);animation:container-rotate 1568ms linear infinite,fade-out 400ms cubic-bezier(0.4, 0, 0.2, 1)}@-webkit-keyframes fade-out{from{opacity:1}to{opacity:0}}@keyframes fade-out{from{opacity:1}to{opacity:0}}.slider{position:relative;height:400px;width:100%}.slider.fullscreen{height:100%;width:100%;position:absolute;top:0;left:0;right:0;bottom:0}.slider.fullscreen ul.slides{height:100%}.slider.fullscreen ul.indicators{z-index:2;bottom:30px}.slider .slides{background-color:#9e9e9e;margin:0;height:400px}.slider .slides li{opacity:0;position:absolute;top:0;left:0;z-index:1;width:100%;height:inherit;overflow:hidden}.slider .slides li img{height:100%;width:100%;background-size:cover;background-position:center}.slider .slides li .caption{color:#fff;position:absolute;top:15%;left:15%;width:70%;opacity:0}.slider .slides li .caption p{color:#e0e0e0}.slider .slides li.active{z-index:2}.slider .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.slider .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:16px;width:16px;margin:0 12px;background-color:#e0e0e0;transition:background-color .3s;border-radius:50%}.slider .indicators .indicator-item.active{background-color:#4CAF50}.carousel{overflow:hidden;position:relative;width:100%;height:400px;perspective:500px;transform-style:preserve-3d;transform-origin:0% 50%}.carousel.carousel-slider{top:0;left:0}.carousel.carousel-slider .carousel-fixed-item{position:absolute;left:0;right:0;bottom:20px;z-index:1}.carousel.carousel-slider .carousel-fixed-item.with-indicators{bottom:68px}.carousel.carousel-slider .carousel-item{width:100%;height:100%;min-height:400px;position:absolute;top:0;left:0}.carousel.carousel-slider .carousel-item h2{font-size:24px;font-weight:500;line-height:32px}.carousel.carousel-slider .carousel-item p{font-size:15px}.carousel .carousel-item{visibility:hidden;width:200px;height:200px;position:absolute;top:0;left:0}.carousel .carousel-item>img{width:100%}.carousel .indicators{position:absolute;text-align:center;left:0;right:0;bottom:0;margin:0}.carousel .indicators .indicator-item{display:inline-block;position:relative;cursor:pointer;height:8px;width:8px;margin:24px 4px;background-color:#616161;transition:background-color .3s;border-radius:50%}.carousel .indicators .indicator-item.active{background-color:rgba(0,0,0,0.87)}.carousel.scrolling .carousel-item .materialboxed,.carousel .carousel-item:not(.active) .materialboxed{pointer-events:none}.tap-target-wrapper{width:800px;height:800px;position:fixed;z-index:1000;visibility:hidden;transition:visibility 0s .3s}.tap-target-wrapper.open{visibility:visible;transition:visibility 0s}.tap-target-wrapper.open .tap-target{transform:scale(1);opacity:.95;transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-wrapper.open .tap-target-wave::before{transform:scale(1)}.tap-target-wrapper.open .tap-target-wave::after{visibility:visible;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;transition:opacity .3s, transform .3s, visibility 0s 1s}.tap-target{position:absolute;font-size:1rem;border-radius:50%;background-color:#ee6e73;box-shadow:0 20px 20px 0 rgba(0,0,0,0.14),0 10px 50px 0 rgba(0,0,0,0.12),0 30px 10px -20px rgba(0,0,0,0.2);width:100%;height:100%;opacity:0;transform:scale(0);transition:transform 0.3s cubic-bezier(0.42, 0, 0.58, 1),opacity 0.3s cubic-bezier(0.42, 0, 0.58, 1)}.tap-target-content{position:relative;display:table-cell}.tap-target-wave{position:absolute;border-radius:50%;z-index:10001}.tap-target-wave::before,.tap-target-wave::after{content:'';display:block;position:absolute;width:100%;height:100%;border-radius:50%;background-color:#ffffff}.tap-target-wave::before{transform:scale(0);transition:transform .3s}.tap-target-wave::after{visibility:hidden;transition:opacity .3s, transform .3s, visibility 0s;z-index:-1}.tap-target-origin{top:50%;left:50%;transform:translate(-50%, -50%);z-index:10002;position:absolute !important}.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small),.tap-target-origin:not(.btn):not(.btn-large):not(.btn-small):hover{background:none}@media only screen and (max-width: 600px){.tap-target,.tap-target-wrapper{width:600px;height:600px}}.pulse{overflow:visible;position:relative}.pulse::before{content:'';display:block;position:absolute;width:100%;height:100%;top:0;left:0;background-color:inherit;border-radius:inherit;transition:opacity .3s, transform .3s;animation:pulse-animation 1s cubic-bezier(0.24, 0, 0.38, 1) infinite;z-index:-1}@keyframes pulse-animation{0%{opacity:1;transform:scale(1)}50%{opacity:0;transform:scale(1.5)}100%{opacity:0;transform:scale(1.5)}}.datepicker-modal{max-width:325px;min-width:300px;max-height:none}.datepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.datepicker-controls{display:flex;justify-content:space-between;width:280px;margin:0 auto}.datepicker-controls .selects-container{display:flex}.datepicker-controls .select-wrapper input{border-bottom:none;text-align:center;margin:0}.datepicker-controls .select-wrapper input:focus{border-bottom:none}.datepicker-controls .select-wrapper .caret{display:none}.datepicker-controls .select-year input{width:50px}.datepicker-controls .select-month input{width:70px}.month-prev,.month-next{margin-top:4px;cursor:pointer;background-color:transparent;border:none}.datepicker-date-display{flex:1 auto;background-color:#26a69a;color:#fff;padding:20px 22px;font-weight:500}.datepicker-date-display .year-text{display:block;font-size:1.5rem;line-height:25px;color:rgba(255,255,255,0.7)}.datepicker-date-display .date-text{display:block;font-size:2.8rem;line-height:47px;font-weight:500}.datepicker-calendar-container{flex:2.5 auto}.datepicker-table{width:280px;font-size:1rem;margin:0 auto}.datepicker-table thead{border-bottom:none}.datepicker-table th{padding:10px 5px;text-align:center}.datepicker-table tr{border:none}.datepicker-table abbr{text-decoration:none;color:#999}.datepicker-table td{border-radius:50%;padding:0}.datepicker-table td.is-today{color:#26a69a}.datepicker-table td.is-selected{background-color:#26a69a;color:#fff}.datepicker-table td.is-outside-current-month,.datepicker-table td.is-disabled{color:rgba(0,0,0,0.3);pointer-events:none}.datepicker-day-button{background-color:transparent;border:none;line-height:38px;display:block;width:100%;border-radius:50%;padding:0 5px;cursor:pointer;color:inherit}.datepicker-day-button:focus{background-color:rgba(43,161,150,0.25)}.datepicker-footer{width:280px;margin:0 auto;padding-bottom:5px;display:flex;justify-content:space-between}.datepicker-cancel,.datepicker-clear,.datepicker-today,.datepicker-done{color:#26a69a;padding:0 1rem}.datepicker-clear{color:#F44336}@media only screen and (min-width : 601px){.datepicker-modal{max-width:625px}.datepicker-container.modal-content{flex-direction:row}.datepicker-date-display{flex:0 1 270px}.datepicker-controls,.datepicker-table,.datepicker-footer{width:320px}.datepicker-day-button{line-height:44px}}.timepicker-modal{max-width:325px;max-height:none}.timepicker-container.modal-content{display:flex;flex-direction:column;padding:0}.text-primary{color:#fff}.timepicker-digital-display{flex:1 auto;background-color:#26a69a;padding:10px;font-weight:300}.timepicker-text-container{font-size:4rem;font-weight:bold;text-align:center;color:rgba(255,255,255,0.6);font-weight:400;position:relative;user-select:none}.timepicker-span-hours,.timepicker-span-minutes,.timepicker-span-am-pm div{cursor:pointer}.timepicker-span-hours{margin-right:3px}.timepicker-span-minutes{margin-left:3px}.timepicker-display-am-pm{font-size:1.3rem;position:absolute;right:1rem;bottom:1rem;font-weight:400}.timepicker-analog-display{flex:2.5 auto}.timepicker-plate{background-color:#eee;border-radius:50%;width:270px;height:270px;overflow:visible;position:relative;margin:auto;margin-top:25px;margin-bottom:5px;user-select:none}.timepicker-canvas,.timepicker-dial{position:absolute;left:0;right:0;top:0;bottom:0}.timepicker-minutes{visibility:hidden}.timepicker-tick{border-radius:50%;color:rgba(0,0,0,0.87);line-height:40px;text-align:center;width:40px;height:40px;position:absolute;cursor:pointer;font-size:15px}.timepicker-tick.active,.timepicker-tick:hover{background-color:rgba(38,166,154,0.25)}.timepicker-dial{transition:transform 350ms, opacity 350ms}.timepicker-dial-out{opacity:0}.timepicker-dial-out.timepicker-hours{transform:scale(1.1, 1.1)}.timepicker-dial-out.timepicker-minutes{transform:scale(0.8, 0.8)}.timepicker-canvas{transition:opacity 175ms}.timepicker-canvas line{stroke:#26a69a;stroke-width:4;stroke-linecap:round}.timepicker-canvas-out{opacity:0.25}.timepicker-canvas-bearing{stroke:none;fill:#26a69a}.timepicker-canvas-bg{stroke:none;fill:#26a69a}.timepicker-footer{margin:0 auto;padding:5px 1rem;display:flex;justify-content:space-between}.timepicker-clear{color:#F44336}.timepicker-close{color:#26a69a}.timepicker-clear,.timepicker-close{padding:0 20px}@media only screen and (min-width : 601px){.timepicker-modal{max-width:600px}.timepicker-container.modal-content{flex-direction:row}.timepicker-text-container{top:32%}.timepicker-display-am-pm{position:relative;right:auto;bottom:auto;text-align:center;margin-top:1.2rem}}.caution-color{background-color:#F44336}.caution-color-text{color:#F44336}.info-color{background-color:#fff9c4}.info-color-text{color:#fff9c4}.contrast-color{background-color:rgba(0,0,0,0.87)}.contrast-color-text{color:rgba(0,0,0,0.87)}.success-color{background-color:#4CAF50}.success-color-text{color:#4CAF50}html{background-color:#fff}a.unmarked{color:rgba(0,0,0,0.87)}.white-text a{color:#eeeeff}div.targetlist{display:grid;grid-template-columns:1fr max-content;align-items:center}div.targetlist>a.nonflex{padding-left:1em;padding-top:1em;padding-bottom:1em;display:inline-block}a.tablerow{display:flex;justify-content:space-between;padding-top:1em;padding-bottom:1em;border-bottom:1px solid rgba(0,0,0,0.12)}a.tablerow .material-icons{vertical-align:bottom;margin-bottom:0.2em}a.tablerow span{display:inline-block}.pagination li a{color:rgba(0,0,0,0.87)}.collection-item{color:rgba(0,0,0,0.87)}.collection-item .secondary-content{color:rgba(0,0,0,0.87)}.collection-item.disabled{color:#616161}.action-checkin,.action-checkout,.action-undo,.action-cancelled-from,.action-cancelled-to,.action-share{cursor:pointer}.config a{cursor:pointer}.navbar-fixed{z-index:1001}.brand-logo span{transition:color 1s}.brand-logo:hover .ca,.brand-logo:hover .ce{color:#a8e3fa !important}.brand-logo:hover .cb,.brand-logo:hover .cd{color:#f5c4ce !important}.wagons span{margin-right:0.5ex;color:#808080}.wagons .wagonclass{font-weight:bold;color:inherit}.wagons .wagonnum{margin-right:0;color:inherit}.wagons .checksum:before{content:"-"}h1{font-size:2.92rem;margin:1.9466666667rem 0 1.168rem 0}h2{font-size:2.28rem;margin:1.52rem 0 .912rem 0}h3{font-size:1.64rem;margin:1.0933333333rem 0 .656rem 0}.geolocation i.material-icons{font-size:16px}ul.suggestions li{padding-top:0.5rem;padding-bottom:0.5rem}.collection.departures>li,.collection.history>li{transition:background .3s;display:grid}.collection.departures>li:not(#now,.history-date-change):hover,.collection.departures>li:focus-within,.collection.history>li:not(#now,.history-date-change):hover,.collection.history>li:focus-within{background-color:#eee;outline:2px solid #039be5}.collection.departures li{grid-template-columns:10ch 10ch 1fr;align-items:center}.collection.departures li#now{background-color:#eee;padding:2rem 20px;grid-template-columns:10ch 1fr}.collection.departures li#now strong{font-weight:bold}.collection.departures li.cancelled{background-color:#FFCDD2;font-style:italic}.collection.departures li.cancelled .dep-line{background-color:transparent;border:1px solid;color:rgba(0,0,0,0.87)}.collection.departures li.cancelled .dep-time::after{content:" ⊖";font-style:normal}.departures .dep-time{color:rgba(0,0,0,0.87)}.departures .dep-time:focus{outline:none}.departures .dep-dest{margin-left:0.8rem}.departures .dep-dest i.material-icons{vertical-align:middle}.departures .dep-dest .followee-checkin{font-size:0.9rem;display:block}.collection.history>li{display:grid;grid-template-columns:10ch 1fr;grid-template-rows:1fr}.collection.history>li a:first-child{align-self:center;text-align:center;display:flex}.collection.history>li.history-date-change{display:block;font-weight:bold}ul.route-history>li{list-style:none;position:relative;display:grid;grid-template-columns:1rem 1fr;gap:0.5rem}ul.route-history>li a{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif}ul.route-history>li strong{font-weight:600}ul.route-history>li i.material-icons[aria-label=nach]{padding-top:0.4rem}ul.route-history>li i.material-icons[aria-label=von]{display:block;transform:rotate(-90deg);height:1rem;margin-top:0.4rem}ul.route-history>li::before{content:'';background:rgba(0,0,0,0.87);position:absolute;width:2px;left:calc( (1rem - 2px) / 2);bottom:0;top:0}ul.route-history>li:first-of-type::before{top:1.3rem}ul.route-history>li:last-of-type::before{bottom:unset;height:0.5rem}.dep-line{text-align:center;padding:.2rem;color:white;background:#424242;border-radius:.2rem;display:inline-block;font-weight:600;line-height:1;height:fit-content;width:fit-content;min-width:6ch;margin:0 auto}.dep-line.Bus,.dep-line.BUS,.dep-line.NachtBus,.dep-line.Niederflurbus,.dep-line.Stadtbus,.dep-line.MetroBus,.dep-line.PlusBus,.dep-line.Landbus,.dep-line.Regionalbus,.dep-line.RegionalBus,.dep-line.SB,.dep-line.ExpressBus,.dep-line.BSV,.dep-line.RVV-Bus-Linie,.dep-line.Buslinie,.dep-line.Omnibus,.dep-line.RegioBus{background-color:#a3167e;border-radius:5rem;padding:.2rem .5rem}.dep-line.RUF,.dep-line.AST,.dep-line.RufTaxi,.dep-line.Rufbus,.dep-line.Linientaxi{background-color:#ffd800;color:black;border-radius:5rem;padding:.2rem .5rem}.dep-line.Fhre,.dep-line.Fh,.dep-line.Schiff,.dep-line.SCH,.dep-line.KAT{background-color:#309fd1;border-radius:5rem;padding:.2rem .5rem}.dep-line.STR,.dep-line.Tram,.dep-line.TRAM,.dep-line.Str,.dep-line.Strb,.dep-line.STB,.dep-line.Straenbahn,.dep-line.NachtTram,.dep-line.Stadtbahn,.dep-line.Niederflurstrab{background-color:#c5161c;border-radius:5rem;padding:.2rem .5rem}.dep-line.S,.dep-line.RS,.dep-line.RER,.dep-line.SKW,.dep-line.METRO,.dep-line.S-Bahn{background-color:#008d4f;border-radius:5rem;padding:.2rem .5rem}.dep-line.U,.dep-line.M,.dep-line.SUBWAY,.dep-line.U-Bahn,.dep-line.UBAHN,.dep-line.Schw-B,.dep-line.Schwebebahn,.dep-line.H-Bahn{background-color:#014e8d;border-radius:5rem;padding:.2rem .5rem}.dep-line.RE,.dep-line.IRE,.dep-line.REX,.dep-line.REGIONAL_FAST_RAIL{background-color:#ff4f00}.dep-line.RB,.dep-line.MEX,.dep-line.TER,.dep-line.R,.dep-line.REGIONAL_RAIL,.dep-line.Regionalzug,.dep-line.R-Bahn,.dep-line.BRB{background-color:#1f4a87}.dep-line.IC,.dep-line.ICE,.dep-line.EC,.dep-line.ECE,.dep-line.D,.dep-line.IR,.dep-line.TGV,.dep-line.OGV,.dep-line.EST,.dep-line.TLK,.dep-line.EIC,.dep-line.HIGHSPEED_RAIL,.dep-line.LONG_DISTANCE{background-color:#ff0404;font-weight:900;font-style:italic;padding:.2rem}.dep-line.RJ,.dep-line.RJX{background-color:#c63131}.dep-line.NJ,.dep-line.EN,.dep-line.NIGHT_RAIL{background-color:#29255b}.dep-line.WB{background-color:#2e85ce}.dep-line.FLX{background-color:#71d800;color:black}.departures.connections li{grid-template-columns:15ch 10ch 1fr}.departures.connections .connect-platform-wrapper{text-align:center}.departures.connections .connect-platform-wrapper span{display:block}.status-card-progress-annot{padding-bottom:2ex;border-bottom:2px dashed #808080}.timeline-in-transit .status-card-progress-annot{border-bottom:none}@media screen and (max-width: 600px){.collection.departures li{grid-template-columns:10ch 1fr}.collection.departures li .dep-line,.collection.departures li .dep-time,.collection.departures li .connect-platform-wrapper{grid-column:1;text-align:center}.collection.departures li .dep-dest{grid-column:2;grid-row:1 / span 2}.departures.connections li{grid-template-columns:15ch 1fr}.departures.connections li .connect-platform-wrapper span{display:inline-block}}a.timeline-link{padding-top:1ex;padding-bottom:1ex}.progress{background-color:#e0e0e0}.progress>.determinate{background-color:#2196F3}
diff --git a/public/static/css/material-icons.css b/public/static/css/material-icons.css
index c059c8f..aed1a60 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/v71/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
+ src: url(/static/v97/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */
src: local('Material Icons'),
local('MaterialIcons-Regular'),
- url(/static/v71/fonts/MaterialIcons-Regular.woff2) format('woff2'),
- url(/static/v71/fonts/MaterialIcons-Regular.woff) format('woff'),
- url(/static/v71/fonts/MaterialIcons-Regular.ttf) format('truetype');
+ url(/static/v97/fonts/MaterialIcons-Regular.woff2) format('woff2'),
+ url(/static/v97/fonts/MaterialIcons-Regular.woff) format('woff'),
+ url(/static/v97/fonts/MaterialIcons-Regular.ttf) format('truetype');
}
.material-icons {
diff --git a/public/static/js/geolocation.js b/public/static/js/geolocation.js
index 03857a1..2082e44 100644
--- a/public/static/js/geolocation.js
+++ b/public/static/js/geolocation.js
@@ -24,7 +24,9 @@ $(document).ready(function() {
const res = $(document.createElement('p'));
$.each(stops, function(i, stop) {
const parts = stop.split(';');
- const node = $('<a class="tablerow" href="/s/' + parts[0] + '?hafas=' + parts[2] + '"><span><i class="material-icons" aria-hidden="true">' + (parseInt(parts[2]) ? 'directions' : 'train') + '</i>' + parts[1] + '</span></a>');
+ const [ eva, name, dbris, efa, hafas, motis ] = parts;
+
+ const node = $('<a class="tablerow" href="/s/' + eva + '?dbris=' + (dbris||0) + '&amp;efa=' + (efa||0) + '&amp;hafas=' + (hafas||0) + '&amp;motis=' + (motis||0) + '"><span><i class="material-icons" aria-hidden="true">' + (!(dbris||efa||hafas||motis) ? 'train' : 'directions') + '</i>' + name + '</span></a>');
node.click(function() {
$('nav .preloader-wrapper').addClass('active');
});
@@ -45,13 +47,40 @@ $(document).ready(function() {
} else {
const res = $(document.createElement('p'));
$.each(data.candidates, function(i, candidate) {
+ let node;
+
+ if (candidate.dbris) {
+ const eva = candidate.eva,
+ name = candidate.name,
+ dbris = candidate.dbris,
+ distance = candidate.distance.toFixed(1);
+ node = $('<a class="tablerow" href="/s/' + eva + '?dbris=' + dbris + '"><span><i class="material-icons" aria-hidden="true">directions</i>' + name + '</span></a>');
+ } else if (candidate.efa) {
+ const eva = candidate.eva,
+ name = candidate.name,
+ efa = candidate.efa,
+ distance = candidate.distance.toFixed(1);
+
+ node = $('<a class="tablerow" href="/s/' + eva + '?efa=' + efa + '"><span><i class="material-icons" aria-hidden="true">directions</i>' + name + '</span></a>');
+ } else if (candidate.hafas) {
+ const eva = candidate.eva,
+ name = candidate.name,
+ hafas = candidate.hafas,
+ distance = candidate.distance.toFixed(1);
+
+ node = $('<a class="tablerow" href="/s/' + eva + '?hafas=' + hafas + '"><span><i class="material-icons" aria-hidden="true">directions</i>' + name + '</span></a>');
+ } else if (candidate.motis) {
+ const { id, name, motis } = candidate;
+
+ node = $('<a class="tablerow" href="/s/' + id + '?motis=' + motis + '"><span><i class="material-icons" aria-hidden="true">directions</i>' + name + '</span></a>');
+ } else {
+ const eva = candidate.eva,
+ name = candidate.name,
+ distance = candidate.distance.toFixed(1);
- const eva = candidate.eva,
- name = candidate.name,
- hafas = candidate.hafas,
- distance = candidate.distance.toFixed(1);
+ node = $('<a class="tablerow" href="/s/' + eva + '"><span><i class="material-icons" aria-hidden="true">train</i>' + name + '</span></a>');
+ }
- const node = $('<a class="tablerow" href="/s/' + eva + '?hafas=' + hafas + '"><span><i class="material-icons" aria-hidden="true">' + (parseInt(hafas) ? 'directions' : 'train') + '</i>' + name + '</span></a>');
node.click(function() {
$('nav .preloader-wrapper').addClass('active');
});
@@ -62,7 +91,8 @@ $(document).ready(function() {
};
const processLocation = function(loc) {
- $.post('/geolocation', {lon: loc.coords.longitude, lat: loc.coords.latitude}, processResult);
+ const backend = $('div.geolocation').data('backend');
+ $.post('/geolocation', {lon: loc.coords.longitude, lat: loc.coords.latitude, backend: backend}, processResult);
};
const processError = function(error) {
@@ -77,7 +107,7 @@ $(document).ready(function() {
}
};
- const geoLocationButton = $('div.geolocation > button');
+ const geoLocationButton = $('div.geolocation > .request');
const recentStops = geoLocationButton.data('recent');
const getGeoLocation = function() {
geoLocationButton.replaceWith($('<p class="geolocationhint">Stationen in der Umgebung:</p><div class="progress"><div class="indeterminate"></div></div>'));
diff --git a/public/static/js/geolocation.min.js b/public/static/js/geolocation.min.js
index 54633f8..0689baf 100644
--- a/public/static/js/geolocation.min.js
+++ b/public/static/js/geolocation.min.js
@@ -1 +1 @@
-$(document).ready(function(){function r(){return $("div.geolocation div.progress")}function e(e){$.post("/geolocation",{lon:e.coords.longitude,lat:e.coords.latitude},o)}function n(e){e.code==e.PERMISSION_DENIED?t("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?t("Standort konnte nicht ermittelt werden","(Service nicht verfügbar)","geolocation.error.POSITION_UNAVAILABLE"):e.code==e.TIMEOUT?t("Standort konnte nicht ermittelt werden","(Timeout)","geolocation.error.TIMEOUT"):t("Standort konnte nicht ermittelt werden","(unbekannter Fehler)","unknown geolocation.error code")}const t=function(e,n,t){var o=$(document.createElement("div")),n=(o.attr("class","error"),o.text(n),$(document.createElement("strong"))),e=(n.text(e+" "),o.prepend(n),$("div.geolocation").append(o),$("div.geolocation").data("recent"));if(e){n=e.split("|");const a=$(document.createElement("p"));$.each(n,function(e,n){n=n.split(";"),n=$('<a class="tablerow" href="/s/'+n[0]+"?hafas="+n[2]+'"><span><i class="material-icons" aria-hidden="true">'+(parseInt(n[2])?"directions":"train")+"</i>"+n[1]+"</span></a>");n.click(function(){$("nav .preloader-wrapper").addClass("active")}),a.append(n)}),$("p.geolocationhint").text("Letzte Ziele:"),r().replaceWith(a)}else r().remove()},o=function(e){if(e.error)t("Backend-Fehler:",e.error,null);else if(0==e.candidates.length)t("Keine Bahnhöfe in 70km Umkreis gefunden","",null);else{const i=$(document.createElement("p"));$.each(e.candidates,function(e,n){var t=n.eva,o=n.name,a=n.hafas,n=(n.distance.toFixed(1),$('<a class="tablerow" href="/s/'+t+"?hafas="+a+'"><span><i class="material-icons" aria-hidden="true">'+(parseInt(a)?"directions":"train")+"</i>"+o+"</span></a>"));n.click(function(){$("nav .preloader-wrapper").addClass("active")}),i.append(n)}),r().replaceWith(i)}},a=$("div.geolocation > button");a.data("recent");function i(){a.replaceWith($('<p class="geolocationhint">Stationen in der Umgebung:</p><div class="progress"><div class="indeterminate"></div></div>')),navigator.geolocation.getCurrentPosition(e,n)}a.length&&(navigator.geolocation?navigator.permissions?navigator.permissions.query({name:"geolocation"}).then(function(e){"prompt"===e.state?a.on("click",i):i()}):a.on("click",i):t("Standortanfragen werden von diesem Browser nicht unterstützt","",null))});
+$(document).ready(function(){function i(){return $("div.geolocation div.progress")}function e(e){var a=$("div.geolocation").data("backend");$.post("/geolocation",{lon:e.coords.longitude,lat:e.coords.latitude,backend:a},t)}function a(e){e.code==e.PERMISSION_DENIED?n("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?n("Standort konnte nicht ermittelt werden","(Service nicht verfügbar)","geolocation.error.POSITION_UNAVAILABLE"):e.code==e.TIMEOUT?n("Standort konnte nicht ermittelt werden","(Timeout)","geolocation.error.TIMEOUT"):n("Standort konnte nicht ermittelt werden","(unbekannter Fehler)","unknown geolocation.error code")}const n=function(e,a,n){var t=$(document.createElement("div")),a=(t.attr("class","error"),t.text(a),$(document.createElement("strong"))),e=(a.text(e+" "),t.prepend(a),$("div.geolocation").append(t),$("div.geolocation").data("recent"));if(e){a=e.split("|");const s=$(document.createElement("p"));$.each(a,function(e,a){var[a,n,t,i,o,r]=a.split(";"),a=$('<a class="tablerow" href="/s/'+a+"?dbris="+(t||0)+"&amp;efa="+(i||0)+"&amp;hafas="+(o||0)+"&amp;motis="+(r||0)+'"><span><i class="material-icons" aria-hidden="true">'+(t||i||o||r?"directions":"train")+"</i>"+n+"</span></a>");a.click(function(){$("nav .preloader-wrapper").addClass("active")}),s.append(a)}),$("p.geolocationhint").text("Letzte Ziele:"),i().replaceWith(s)}else i().remove()},t=function(e){if(e.error)n("Backend-Fehler:",e.error,null);else if(0==e.candidates.length)n("Keine Bahnhöfe in 70km Umkreis gefunden","",null);else{const r=$(document.createElement("p"));$.each(e.candidates,function(e,a){let n;var t,i,o;(n=a.dbris?(i=a.eva,o=a.name,t=a.dbris,a.distance.toFixed(1),$('<a class="tablerow" href="/s/'+i+"?dbris="+t+'"><span><i class="material-icons" aria-hidden="true">directions</i>'+o+"</span></a>")):a.efa?(i=a.eva,t=a.name,o=a.efa,a.distance.toFixed(1),$('<a class="tablerow" href="/s/'+i+"?efa="+o+'"><span><i class="material-icons" aria-hidden="true">directions</i>'+t+"</span></a>")):a.hafas?(i=a.eva,o=a.name,t=a.hafas,a.distance.toFixed(1),$('<a class="tablerow" href="/s/'+i+"?hafas="+t+'"><span><i class="material-icons" aria-hidden="true">directions</i>'+o+"</span></a>")):a.motis?({id:i,name:t,motis:o}=a,$('<a class="tablerow" href="/s/'+i+"?motis="+o+'"><span><i class="material-icons" aria-hidden="true">directions</i>'+t+"</span></a>")):(i=a.eva,o=a.name,a.distance.toFixed(1),$('<a class="tablerow" href="/s/'+i+'"><span><i class="material-icons" aria-hidden="true">train</i>'+o+"</span></a>"))).click(function(){$("nav .preloader-wrapper").addClass("active")}),r.append(n)}),i().replaceWith(r)}},o=$("div.geolocation > .request");o.data("recent");function r(){o.replaceWith($('<p class="geolocationhint">Stationen in der Umgebung:</p><div class="progress"><div class="indeterminate"></div></div>')),navigator.geolocation.getCurrentPosition(e,a)}o.length&&(navigator.geolocation?navigator.permissions?navigator.permissions.query({name:"geolocation"}).then(function(e){"prompt"===e.state?o.on("click",r):r()}):o.on("click",r):n("Standortanfragen werden von diesem Browser nicht unterstützt","",null))});
diff --git a/public/static/js/travelynx-actions.js b/public/static/js/travelynx-actions.js
index 48e878f..370aa33 100644
--- a/public/static/js/travelynx-actions.js
+++ b/public/static/js/travelynx-actions.js
@@ -75,13 +75,16 @@ function hhmm(epoch) {
return (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m);
}
function odelay(sched, rt) {
+ if (sched == 0) {
+ return '';
+ }
if (sched < rt) {
- return ' (+' + ((rt - sched) / 60) + ')';
+ return ' (+' + Math.round((rt - sched) / 60) + ')';
}
else if (sched == rt) {
return '';
}
- return ' (' + ((rt - sched) / 60) + ')';
+ return ' (' + Math.round((rt - sched) / 60) + ')';
}
function tvly_run(link, req, err_callback) {
@@ -191,8 +194,13 @@ function tvly_reg_handlers() {
var link = $(this);
var req = {
action: 'checkin',
+ dbris: link.data('dbris'),
+ efa: link.data('efa'),
+ hafas: link.data('hafas'),
+ motis: link.data('motis'),
station: link.data('station'),
train: link.data('train'),
+ suffix: link.data('suffix'),
dest: link.data('dest'),
ts: link.data('ts'),
};
@@ -202,6 +210,10 @@ function tvly_reg_handlers() {
var link = $(this);
var req = {
action: 'checkout',
+ dbris: link.data('dbris'),
+ efa: link.data('efa'),
+ hafas: link.data('hafas'),
+ motis: link.data('motis'),
station: link.data('station'),
force: link.data('force'),
};
@@ -232,6 +244,10 @@ function tvly_reg_handlers() {
var link = $(this);
var req = {
action: 'cancelled_from',
+ dbris: link.data('dbris'),
+ efa: link.data('efa'),
+ hafas: link.data('hafas'),
+ motis: link.data('motis'),
station: link.data('station'),
ts: link.data('ts'),
train: link.data('train'),
@@ -242,6 +258,10 @@ function tvly_reg_handlers() {
var link = $(this);
var req = {
action: 'cancelled_to',
+ dbris: link.data('dbris'),
+ efa: link.data('efa'),
+ hafas: link.data('hafas'),
+ motis: link.data('motis'),
station: link.data('station'),
force: true,
};
@@ -308,18 +328,18 @@ $(document).ready(function() {
$('nav .preloader-wrapper').addClass('active');
});
$('a[href="#now"]').keydown(function(event) {
- // also trigger click handler on keyboard enter
- if (event.keyCode == 13) {
- event.preventDefault();
- event.target.click();
- }
+ // also trigger click handler on keyboard enter
+ if (event.keyCode == 13) {
+ event.preventDefault();
+ event.target.click();
+ }
});
$('a[href="#now"]').click(function(event) {
- event.preventDefault();
- $('nav .preloader-wrapper').removeClass('active');
- now_el = $('#now')[0];
- now_el.previousElementSibling.querySelector(".dep-time").focus();
- now_el.scrollIntoView({behavior: "smooth", block: "center"});
+ event.preventDefault();
+ $('nav .preloader-wrapper').removeClass('active');
+ now_el = $('#now')[0];
+ now_el.previousElementSibling.querySelector(".dep-time").focus();
+ now_el.scrollIntoView({behavior: "smooth", block: "center"});
});
const elems = document.querySelectorAll('.carousel');
const instances = M.Carousel.init(elems, {
diff --git a/public/static/js/travelynx-actions.min.js b/public/static/js/travelynx-actions.min.js
index 8b99a82..b237eb8 100644
--- a/public/static/js/travelynx-actions.min.js
+++ b/public/static/js/travelynx-actions.min.js
@@ -1 +1 @@
-var j_departure=0,j_duration=0,j_arrival=0,j_dest="",j_stops=[],j_token="";function setTheme(t){localStorage.setItem("theme",t),otherTheme.hasOwnProperty(t)||(t=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"),addStyleSheet(t,"theme")}function upd_journey_data(){$(".countdown").each(function(){var t=$(this).data("token"),t=(t&&(j_token=t),$(this).data("journey")),t=(t&&(t=t.split(";"),j_departure=parseInt(t[0]),j_arrival=parseInt(t[1]),j_duration=j_arrival-j_departure),$(this).data("dest")),e=(t&&(j_dest=t),$(this).data("route"));if(e)for(var a in e=e.split("|"),j_stops=[],e){for(var n=e[a].split(";"),o=1;o<5;o++)n[o]=parseInt(n[o]);j_stops.push(n)}})}function upd_countdown(){var t=Date.now()/1e3;t<j_departure?$(".countdown").text("Abfahrt in "+Math.round((j_departure-t)/60)+" Minuten"):0<j_arrival&&(t<j_arrival?120<=(t=Math.round((j_arrival-t)/60))?$(".countdown").text("Ankunft in "+Math.floor(t/60)+" Stunden und "+t%60+" Minuten"):60<=t?$(".countdown").text("Ankunft in 1 Stunde und "+t%60+" Minuten"):$(".countdown").text("Ankunft in "+t+" Minuten"):$(".countdown").text("Ziel erreicht"))}function hhmm(t){var t=new Date(1e3*t),e=t.getHours(),t=t.getMinutes();return(e<10?"0"+e:e)+":"+(t<10?"0"+t:t)}function odelay(t,e){return t<e?" (+"+(e-t)/60+")":t==e?"":" ("+(e-t)/60+")"}function tvly_run(e,t,a){var n='<i class="material-icons">error</i>',o=e.data("tr")?$('<tr><td colspan="'+e.data("tr")+'"><div class="progress"><div class="indeterminate"></div></div></td></tr>'):$('<div class="progress"><div class="indeterminate"></div></div>');e.hide(),e.after(o),$.post("/action",t,function(t){t.success?$(location).attr("href",t.redirect_to):(M.toast({html:n+" "+t.error}),o.remove(),a&&a(),e.append(" "+n),e.show())})}function tvly_update(){$.get("/ajax/status_card.html",function(t){$(".statuscol").html(t),tvly_reg_handlers(),upd_journey_data(),setTimeout(tvly_update,4e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),upd_countdown(),setTimeout(tvly_update,5e3)})}function tvly_update_public(){var t,e=0;$(".publicstatuscol").each(function(){t=$(this).data("user"),e=$(this).data("profile")}),$.get("/ajax/status/"+t+".html",{token:j_token,profile:e},function(t){$(".publicstatuscol").html(t),upd_journey_data(),setTimeout(tvly_update_public,4e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),upd_countdown(),setTimeout(tvly_update_public,5e3)})}function tvly_update_timeline(){$.get("/timeline/in-transit",{ajax:1},function(t){$(".timeline-in-transit").html(t),setTimeout(tvly_update_timeline,6e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),setTimeout(tvly_update_timeline,1e4)})}function tvly_journey_progress(){var t=Date.now()/1e3,e=0;if(0<j_duration){for(stop in 1<(e=(e=1-(j_arrival-t)/j_duration)<0?0:e)&&(e=1),$(".progress .determinate").css("width",100*e+"%"),j_stops){var a=j_stops[stop][0],n=j_stops[stop][1],o=j_stops[stop][2],i=j_stops[stop][3],r=j_stops[stop][4];if(a==j_dest){$(".next-stop").html("");break}if(0!=o&&0<o-t){$(".next-stop").html(a+"<br/>"+hhmm(o)+odelay(n,o));break}if(0!=r&&0<r-t){0!=o?$(".next-stop").html(a+"<br/>"+hhmm(o)+" → "+hhmm(r)+odelay(i,r)):$(".next-stop").html(a+"<br/>"+hhmm(r)+odelay(i,r));break}}setTimeout(tvly_journey_progress,5e3)}}function tvly_reg_handlers(){$(".action-checkin").click(function(){var t=$(this),e={action:"checkin",station:t.data("station"),train:t.data("train"),dest:t.data("dest"),ts:t.data("ts")};tvly_run(t,e)}),$(".action-checkout").click(function(){var t=$(this),e={action:"checkout",station:t.data("station"),force:t.data("force")};tvly_run(t,e,function(){t.data("force")||(t.append(" – Ohne Echtzeitdaten auschecken?"),t.data("force",!0))})}),$(".action-undo").click(function(){var t=$(this),e=Date.now()/1e3,a=parseInt(t.data("checkints")),n={action:"undo",undo_id:t.data("id")},o=!0;(o=900<e-a?confirm("Checkin wirklich rückgängig machen? Er kann ggf. nicht wiederholt werden."):o)&&tvly_run(t,n)}),$(".action-cancelled-from").click(function(){var t=$(this),e={action:"cancelled_from",station:t.data("station"),ts:t.data("ts"),train:t.data("train")};tvly_run(t,e)}),$(".action-cancelled-to").click(function(){var t=$(this),e={action:"cancelled_to",station:t.data("station"),force:!0};tvly_run(t,e)}),$(".action-delete").click(function(){var t=$(this),e={action:"delete",id:t.data("id"),checkin:t.data("checkin"),checkout:t.data("checkout")};confirm("Diese Fahrt wirklich löschen? Der Eintrag wird sofort aus der Datenbank entfernt und kann nicht wiederhergestellt werden.")&&tvly_run(t,e)}),$(".action-share").click(function(){var t=$(this).data("text"),e=$(this).data("url");navigator.share?(shareObj={text:t},e&&(shareObj.url=e),navigator.share(shareObj)):(e&&(t+=" "+e),(e=document.createElement("textarea")).value=t,e.setAttribute("readonly",""),e.style.position="absolute",e.style.left="-9999px",document.body.appendChild(e),e.select(),e.setSelectionRange(0,99999),document.execCommand("copy"),document.body.removeChild(e),M.toast({html:"Text kopiert: „"+t+"“"}))})}$(document).ready(function(){tvly_reg_handlers(),$(".statuscol .autorefresh").length&&(upd_journey_data(),setTimeout(tvly_update,4e4),setTimeout(tvly_journey_progress,5e3)),$(".publicstatuscol .autorefresh").length&&(upd_journey_data(),setTimeout(tvly_update_public,4e4),setTimeout(tvly_journey_progress,5e3)),$(".timeline-in-transit .autorefresh").length&&setTimeout(tvly_update_timeline,6e4),$("a[href]").click(function(){$("nav .preloader-wrapper").addClass("active")}),$('a[href="#now"]').keydown(function(t){13==t.keyCode&&(t.preventDefault(),t.target.click())}),$('a[href="#now"]').click(function(t){t.preventDefault(),$("nav .preloader-wrapper").removeClass("active"),(now_el=$("#now")[0]).previousElementSibling.querySelector(".dep-time").focus(),now_el.scrollIntoView({behavior:"smooth",block:"center"})});var t=document.querySelectorAll(".carousel");M.Carousel.init(t,{fullWidth:!0,indicators:!0})});
+var j_departure=0,j_duration=0,j_arrival=0,j_dest="",j_stops=[],j_token="";function setTheme(t){localStorage.setItem("theme",t),otherTheme.hasOwnProperty(t)||(t=window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"),addStyleSheet(t,"theme")}function upd_journey_data(){$(".countdown").each(function(){var t=$(this).data("token"),t=(t&&(j_token=t),$(this).data("journey")),t=(t&&(t=t.split(";"),j_departure=parseInt(t[0]),j_arrival=parseInt(t[1]),j_duration=j_arrival-j_departure),$(this).data("dest")),e=(t&&(j_dest=t),$(this).data("route"));if(e)for(var a in e=e.split("|"),j_stops=[],e){for(var n=e[a].split(";"),i=1;i<5;i++)n[i]=parseInt(n[i]);j_stops.push(n)}})}function upd_countdown(){var t=Date.now()/1e3;t<j_departure?$(".countdown").text("Abfahrt in "+Math.round((j_departure-t)/60)+" Minuten"):0<j_arrival&&(t<j_arrival?120<=(t=Math.round((j_arrival-t)/60))?$(".countdown").text("Ankunft in "+Math.floor(t/60)+" Stunden und "+t%60+" Minuten"):60<=t?$(".countdown").text("Ankunft in 1 Stunde und "+t%60+" Minuten"):$(".countdown").text("Ankunft in "+t+" Minuten"):$(".countdown").text("Ziel erreicht"))}function hhmm(t){var t=new Date(1e3*t),e=t.getHours(),t=t.getMinutes();return(e<10?"0"+e:e)+":"+(t<10?"0"+t:t)}function odelay(t,e){return 0==t?"":t<e?" (+"+Math.round((e-t)/60)+")":t==e?"":" ("+Math.round((e-t)/60)+")"}function tvly_run(e,t,a){var n='<i class="material-icons">error</i>',i=e.data("tr")?$('<tr><td colspan="'+e.data("tr")+'"><div class="progress"><div class="indeterminate"></div></div></td></tr>'):$('<div class="progress"><div class="indeterminate"></div></div>');e.hide(),e.after(i),$.post("/action",t,function(t){t.success?$(location).attr("href",t.redirect_to):(M.toast({html:n+" "+t.error}),i.remove(),a&&a(),e.append(" "+n),e.show())})}function tvly_update(){$.get("/ajax/status_card.html",function(t){$(".statuscol").html(t),tvly_reg_handlers(),upd_journey_data(),setTimeout(tvly_update,4e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),upd_countdown(),setTimeout(tvly_update,5e3)})}function tvly_update_public(){var t,e=0;$(".publicstatuscol").each(function(){t=$(this).data("user"),e=$(this).data("profile")}),$.get("/ajax/status/"+t+".html",{token:j_token,profile:e},function(t){$(".publicstatuscol").html(t),upd_journey_data(),setTimeout(tvly_update_public,4e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),upd_countdown(),setTimeout(tvly_update_public,5e3)})}function tvly_update_timeline(){$.get("/timeline/in-transit",{ajax:1},function(t){$(".timeline-in-transit").html(t),setTimeout(tvly_update_timeline,6e4)}).fail(function(){$(".sync-failed-marker").css("display","block"),setTimeout(tvly_update_timeline,1e4)})}function tvly_journey_progress(){var t=Date.now()/1e3,e=0;if(0<j_duration){for(stop in 1<(e=(e=1-(j_arrival-t)/j_duration)<0?0:e)&&(e=1),$(".progress .determinate").css("width",100*e+"%"),j_stops){var a=j_stops[stop][0],n=j_stops[stop][1],i=j_stops[stop][2],o=j_stops[stop][3],r=j_stops[stop][4];if(a==j_dest){$(".next-stop").html("");break}if(0!=i&&0<i-t){$(".next-stop").html(a+"<br/>"+hhmm(i)+odelay(n,i));break}if(0!=r&&0<r-t){0!=i?$(".next-stop").html(a+"<br/>"+hhmm(i)+" → "+hhmm(r)+odelay(o,r)):$(".next-stop").html(a+"<br/>"+hhmm(r)+odelay(o,r));break}}setTimeout(tvly_journey_progress,5e3)}}function tvly_reg_handlers(){$(".action-checkin").click(function(){var t=$(this),e={action:"checkin",dbris:t.data("dbris"),efa:t.data("efa"),hafas:t.data("hafas"),motis:t.data("motis"),station:t.data("station"),train:t.data("train"),suffix:t.data("suffix"),dest:t.data("dest"),ts:t.data("ts")};tvly_run(t,e)}),$(".action-checkout").click(function(){var t=$(this),e={action:"checkout",dbris:t.data("dbris"),efa:t.data("efa"),hafas:t.data("hafas"),motis:t.data("motis"),station:t.data("station"),force:t.data("force")};tvly_run(t,e,function(){t.data("force")||(t.append(" – Ohne Echtzeitdaten auschecken?"),t.data("force",!0))})}),$(".action-undo").click(function(){var t=$(this),e=Date.now()/1e3,a=parseInt(t.data("checkints")),n={action:"undo",undo_id:t.data("id")},i=!0;(i=900<e-a?confirm("Checkin wirklich rückgängig machen? Er kann ggf. nicht wiederholt werden."):i)&&tvly_run(t,n)}),$(".action-cancelled-from").click(function(){var t=$(this),e={action:"cancelled_from",dbris:t.data("dbris"),efa:t.data("efa"),hafas:t.data("hafas"),motis:t.data("motis"),station:t.data("station"),ts:t.data("ts"),train:t.data("train")};tvly_run(t,e)}),$(".action-cancelled-to").click(function(){var t=$(this),e={action:"cancelled_to",dbris:t.data("dbris"),efa:t.data("efa"),hafas:t.data("hafas"),motis:t.data("motis"),station:t.data("station"),force:!0};tvly_run(t,e)}),$(".action-delete").click(function(){var t=$(this),e={action:"delete",id:t.data("id"),checkin:t.data("checkin"),checkout:t.data("checkout")};confirm("Diese Fahrt wirklich löschen? Der Eintrag wird sofort aus der Datenbank entfernt und kann nicht wiederhergestellt werden.")&&tvly_run(t,e)}),$(".action-share").click(function(){var t=$(this).data("text"),e=$(this).data("url");navigator.share?(shareObj={text:t},e&&(shareObj.url=e),navigator.share(shareObj)):(e&&(t+=" "+e),(e=document.createElement("textarea")).value=t,e.setAttribute("readonly",""),e.style.position="absolute",e.style.left="-9999px",document.body.appendChild(e),e.select(),e.setSelectionRange(0,99999),document.execCommand("copy"),document.body.removeChild(e),M.toast({html:"Text kopiert: „"+t+"“"}))})}$(document).ready(function(){tvly_reg_handlers(),$(".statuscol .autorefresh").length&&(upd_journey_data(),setTimeout(tvly_update,4e4),setTimeout(tvly_journey_progress,5e3)),$(".publicstatuscol .autorefresh").length&&(upd_journey_data(),setTimeout(tvly_update_public,4e4),setTimeout(tvly_journey_progress,5e3)),$(".timeline-in-transit .autorefresh").length&&setTimeout(tvly_update_timeline,6e4),$("a[href]").click(function(){$("nav .preloader-wrapper").addClass("active")}),$('a[href="#now"]').keydown(function(t){13==t.keyCode&&(t.preventDefault(),t.target.click())}),$('a[href="#now"]').click(function(t){t.preventDefault(),$("nav .preloader-wrapper").removeClass("active"),(now_el=$("#now")[0]).previousElementSibling.querySelector(".dep-time").focus(),now_el.scrollIntoView({behavior:"smooth",block:"center"})});var t=document.querySelectorAll(".carousel");M.Carousel.init(t,{fullWidth:!0,indicators:!0})});
diff --git a/public/static/manifest.json b/public/static/manifest.json
index 9b85950..e428bfe 100644
--- a/public/static/manifest.json
+++ b/public/static/manifest.json
@@ -3,27 +3,27 @@
"short_name": "Travelynx",
"scope": "/",
"icons": [{
- "src": "/static/v71/icons/icon-128x128.png",
+ "src": "/static/v97/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
}, {
- "src": "/static/v71/icons/icon-144x144.png",
+ "src": "/static/v97/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
}, {
- "src": "/static/v71/icons/icon-152x152.png",
+ "src": "/static/v97/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
}, {
- "src": "/static/v71/icons/icon-192x192.png",
+ "src": "/static/v97/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
}, {
- "src": "/static/v71/icons/icon-256x256.png",
+ "src": "/static/v97/icons/icon-256x256.png",
"sizes": "256x256",
"type": "image/png"
}, {
- "src": "/static/v71/icons/icon-512x512.png",
+ "src": "/static/v97/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}],
diff --git a/public/static/v70 b/public/static/v96
index 945c9b4..945c9b4 120000
--- a/public/static/v70
+++ b/public/static/v96
diff --git a/public/static/v71 b/public/static/v97
index 945c9b4..945c9b4 120000
--- a/public/static/v71
+++ b/public/static/v97
diff --git a/sass/src/common/local.scss b/sass/src/common/local.scss
index c3fe29c..320c6d0 100644
--- a/sass/src/common/local.scss
+++ b/sass/src/common/local.scss
@@ -122,12 +122,12 @@ ul.suggestions {
}
.departures .dep-dest {
margin-left: 0.8rem;
+ i.material-icons {
+ vertical-align: middle;
+ }
.followee-checkin {
font-size: 0.9rem;
display: block;
- i.material-icons {
- vertical-align: middle;
- }
}
}
@@ -208,31 +208,42 @@ ul.route-history > li {
width: fit-content;
min-width: 6ch;
margin: 0 auto;
-
- &.Bus, &.RUF, &.AST {
+
+ &.Bus, &.BUS, &.NachtBus, &.Niederflurbus, &.Stadtbus, &.MetroBus, &.PlusBus, &.Landbus, &.Regionalbus, &.RegionalBus, &.SB, &.ExpressBus, &.BSV, &.RVV-Bus-Linie, &.Buslinie, &.Omnibus, &.RegioBus {
background-color: #a3167e;
border-radius: 5rem;
padding: .2rem .5rem;
}
- &.STR {
+ &.RUF, &.AST, &.RufTaxi, &.Rufbus, &.Linientaxi {
+ background-color: #ffd800;
+ color: black;
+ border-radius: 5rem;
+ padding: .2rem .5rem;
+ }
+ &.Fhre, &.Fh, &.Schiff, &.SCH, &.KAT {
+ background-color: #309fd1;
+ border-radius: 5rem;
+ padding: .2rem .5rem;
+ }
+ &.STR, &.Tram, &.TRAM, &.Str, &.Strb, &.STB, &.Straenbahn, &.NachtTram, &.Stadtbahn, &.Niederflurstrab {
background-color: #c5161c;
border-radius: 5rem;
padding: .2rem .5rem;
}
- &.S, &.RS, &.RER, &.SKW {
+ &.S, &.RS, &.RER, &.SKW, &.METRO, &.S-Bahn {
background-color: #008d4f;
border-radius: 5rem;
padding: .2rem .5rem;
}
- &.U, &.STB, &.M {
+ &.U, &.M, &.SUBWAY, &.U-Bahn, &.UBAHN, &.Schw-B, &.Schwebebahn, &.H-Bahn {
background-color: #014e8d;
border-radius: 5rem;
padding: .2rem .5rem;
}
- &.RE, &.IRE, &.REX {
+ &.RE, &.IRE, &.REX, &.REGIONAL_FAST_RAIL {
background-color: #ff4f00;
}
- &.RB, &.MEX, &.TER, &.R {
+ &.RB, &.MEX, &.TER, &.R, &.REGIONAL_RAIL, &.Regionalzug, &.R-Bahn, &.BRB {
background-color: #1f4a87;
}
// DE
@@ -242,7 +253,9 @@ ul.route-history > li {
// FR
&.TGV, &.OGV, &.EST,
// PL
- &.TLK, &.EIC {
+ &.TLK, &.EIC,
+ // MOTIS
+ &.HIGHSPEED_RAIL, &.LONG_DISTANCE {
background-color: #ff0404;
font-weight: 900;
font-style: italic;
@@ -251,7 +264,7 @@ ul.route-history > li {
&.RJ, &.RJX {
background-color: #c63131;
}
- &.NJ, &.EN {
+ &.NJ, &.EN, &.NIGHT_RAIL {
background-color: #29255b;
}
&.WB {
@@ -275,6 +288,17 @@ ul.route-history > li {
}
}
+.status-card-progress-annot {
+ padding-bottom: 2ex;
+ border-bottom: 2px dashed #808080;
+}
+
+.timeline-in-transit {
+ .status-card-progress-annot {
+ border-bottom: none;
+ }
+}
+
@media screen and (max-width: 600px) {
.collection.departures li {
@@ -295,3 +319,8 @@ ul.route-history > li {
}
}
}
+
+a.timeline-link {
+ padding-top: 1ex;
+ padding-bottom: 1ex;
+}
diff --git a/share/ice_names.json b/share/ice_names.json
deleted file mode 100755
index be386b8..0000000
--- a/share/ice_names.json
+++ /dev/null
@@ -1,254 +0,0 @@
-{
-"101": "Gießen",
-"102": "Jever",
-"103": "Neu-Isenburg",
-"104": "Fulda",
-"105": "Offenbach am Main",
-"106": "Itzehoe",
-"107": "Plattling",
-"108": "Lichtenfels",
-"110": "Gelsenkirchen",
-"112": "Memmingen",
-"113": "Frankenthal/Pfalz",
-"114": "Friedrichshafen",
-"115": "Regensburg",
-"116": "Pforzheim",
-"117": "Hof",
-"118": "Gelnhausen",
-"119": "Osnabrück",
-"120": "Lüneburg",
-"152": "Hanau",
-"153": "Neumünster",
-"154": "Flensburg",
-"155": "Rosenheim",
-"156": "Heppenheim/Bergstraße",
-"157": "Landshut",
-"158": "Gütersloh",
-"159": "Bad Oldesloe",
-"160": "Mülheim an der Ruhr",
-"161": "Bebra",
-"162": "Geisenheim/Rheingau",
-"166": "Gelnhausen",
-"167": "Garmisch-Partenkirchen",
-"168": "Crailsheim",
-"169": "Worms",
-"171": "Heusenstamm",
-"172": "Aschaffenburg",
-"173": "Basel",
-"174": "Zürich",
-"175": "Nürnberg",
-"176": "Bremen",
-"177": "Rendsburg",
-"178": "Bremerhaven",
-"180": "Castrop-Rauxel",
-"181": "Interlaken",
-"182": "Rüdesheim am Rhein",
-"183": "Timmendorfer Strand",
-"184": "Bruchsal",
-"185": "Freilassing",
-"186": "Chur",
-"187": "Mühldorf a. Inn",
-"188": "Hildesheim",
-"190": "Ludwigshafen am Rhein",
-"201": "Rheinsberg",
-"202": "Wuppertal",
-"203": "Cottbus/Chóśebuz",
-"204": "Bielefeld",
-"205": "Zwickau",
-"206": "Magdeburg",
-"207": "Stendal",
-"208": "Bonn",
-"209": "Riesa",
-"210": "Fontanestadt Neuruppin",
-"211": "Uelzen",
-"212": "Potsdam",
-"213": "Nauen",
-"214": "Hamm (Westf.)",
-"215": "Bitterfeld-Wolfen",
-"216": "Dessau",
-"217": "Bergen auf Rügen",
-"218": "Braunschweig",
-"219": "Hagen",
-"220": "Meiningen",
-"221": "Lübbenau/Spreewald",
-"222": "Eberswalde",
-"223": "Schwerin",
-"224": "Saalfeld (Saale)",
-"225": "Oldenburg (Oldb)",
-"226": "Lutherstadt Wittenberg",
-"227": "Ludwigslust",
-"228": "Altenburg",
-"229": "Templin",
-"230": "Delitzsch",
-"231": "Brandenburg an der Havel",
-"232": "Frankfurt (Oder)",
-"233": "Ulm",
-"234": "Minden",
-"235": "Görlitz",
-"236": "Jüterbog",
-"237": "Neustrelitz",
-"238": "Saarbrücken",
-"239": "Essen",
-"240": "Bochum",
-"241": "Bad Hersfeld",
-"242": "Quedlinburg",
-"243": "Bautzen/Budyšin",
-"244": "Koblenz",
-"301": "Freiburg im Breisgau",
-"302": "Hansestadt Lübeck",
-"303": "Dortmund",
-"304": "München",
-"305": "Baden-Baden",
-"306": "Nördlingen",
-"307": "Oberhausen",
-"308": "Murnau am Staffelsee",
-"309": "Aalen",
-"310": "Wolfsburg",
-"311": "Wiesbaden",
-"312": "Montabaur",
-"313": "Treuchtlingen",
-"314": "Bergisch Gladbach",
-"315": "Singen (Hohentwiel)",
-"316": "Siegburg",
-"317": "Recklinghausen",
-"318": "Münster (Westf.)",
-"319": "Duisburg",
-"320": "Weil am Rhein",
-"321": "Krefeld",
-"322": "Solingen",
-"323": "Schaffhausen",
-"324": "Fürth",
-"325": "Ravensburg",
-"326": "Neunkirchen",
-"327": "Siegen",
-"328": "Aachen",
-"330": "Göttingen",
-"331": "Westerland/Sylt",
-"332": "Augsburg",
-"333": "Goslar",
-"334": "Offenburg",
-"335": "Konstanz",
-"336": "Ingolstadt",
-"337": "Stuttgart",
-"351": "Herford",
-"352": "Mönchengladbach",
-"353": "Neu-Ulm",
-"354": "Mittenwald",
-"355": "Tuttlingen",
-"357": "Esslingen am Neckar",
-"358": "St. Ingbert",
-"359": "Leverkusen",
-"360": "Linz am Rhein",
-"361": "Celle",
-"362": "Schwerte (Ruhr)",
-"363": "Weilheim i. OB",
-"1101": "Neustadt an der Weinstraße",
-"1102": "Neubrandenburg",
-"1103": "Paderborn",
-"1104": "Erfurt",
-"1105": "Dresden",
-"1107": "Pirna",
-"1108": "Berlin",
-"1109": "Güstrow",
-"1110": "Naumburg (Saale)",
-"1111": "Hansestadt Wismar",
-"1112": "Freie und Hansestadt Hamburg",
-"1113": "Hansestadt Stralsund",
-"1117": "Erlangen",
-"1118": "Plauen/Vogtland",
-"1119": "Meißen",
-"1125": "Arnstadt",
-"1126": "Leipzig",
-"1127": "Weimar",
-"1128": "Reutlingen",
-"1129": "Kiel",
-"1130": "Jena",
-"1131": "Trier",
-"1132": "Wittenberge",
-"1151": "Elsterwerda",
-"1152": "Travemünde",
-"1153": "Ilmenau",
-"1154": "Sonneberg",
-"1155": "Mühlhausen/Thüringen",
-"1156": "Waren (Müritz)",
-"1157": "Innsbruck",
-"1158": "Falkenberg/Elster",
-"1159": "Passau",
-"1160": "Markt Holzkirchen",
-"1161": "Andernach",
-"1162": "Vaihingen an der Enz",
-"1163": "Ostseebad Binz",
-"1164": "Rödental",
-"1165": "Bad Oeynhausen",
-"1166": "Bingen am Rhein",
-"1167": "Traunstein",
-"1168": "Ellwangen",
-"1169": "Tutzing",
-"1170": "Prenzlau",
-"1171": "Oschatz",
-"1172": "Bamberg",
-"1173": "Halle (Saale)",
-"1174": "Hansestadt Warburg",
-"1175": "Villingen-Schwenningen",
-"1176": "Coburg",
-"1177": "Rathenow",
-"1178": "Ostseebad Warnemünde",
-"1180": "Darmstadt",
-"1181": "Horb am Neckar",
-"1182": "Mainz",
-"1183": "Oberursel (Taunus)",
-"1184": "Kaiserslautern",
-"1190": "Wien",
-"1191": "Salzburg",
-"1192": "Linz",
-"1501": "Eisenach",
-"1502": "Karlsruhe",
-"1503": "Altenbeken",
-"1504": "Heidelberg",
-"1505": "Marburg/Lahn",
-"1506": "Kassel",
-"1520": "Gotha",
-"1521": "Homburg/Saar",
-"1522": "Torgau",
-"1523": "Hansestadt Greifswald",
-"1524": "Hansestadt Rostock",
-"2853": "Nationalpark Sächsische Schweiz",
-"2865": "Remstal",
-"2868": "Nationalpark Niedersächsisches Wattenmeer",
-"2871": "Leipziger Neuseenland",
-"2874": "Oberer Neckar",
-"2875": "Magdeburger Börde",
-"4103": "Allgäu",
-"4111": "Gäu",
-"4114": "Dresden Elbland",
-"4117": "Mecklenburgische Ostseeküste",
-"4601": "Europa/Europe",
-"4602": "Euregio Maas-Rhein",
-"4603": "Mannheim",
-"4604": "Brussel/Bruxelles",
-"4607": "Hannover",
-"4610": "Frankfurt am Main",
-"4611": "Düsseldorf",
-"4651": "Amsterdam",
-"4652": "Arnhem",
-"4680": "Würzburg",
-"4682": "Köln",
-"4683": "Limburg an der Lahn",
-"4684": "Forbach-Lorraine",
-"4685": "Schwäbisch Hall",
-"4712": "Dillingen a.d. Donau",
-"4710": "Ansbach",
-"4717": "Paris",
-"8007": "Rheinland",
-"9006": "Martin Luther",
-"9018": "Freistaat Bayern",
-"9025": "Nordrhein-Westfalen",
-"9026": "Zürichsee",
-"9028": "Freistaat Sachsen",
-"9041": "Baden-Württemberg",
-"9046": "Female ICE",
-"9050": "Metropole Ruhr",
-"9202": "Schleswig-Holstein",
-"9457": "Bundesrepublik Deutschland",
-"9481": "Rheinland-Pfalz"
-}
diff --git a/t/11-journey-stats.t b/t/11-journey-stats.t
index 9853b85..4623402 100644
--- a/t/11-journey-stats.t
+++ b/t/11-journey-stats.t
@@ -81,7 +81,7 @@ $t->post_ok(
csrf_token => $csrf_token,
action => 'save',
train => 'RE 42 11238',
- dep_station => 'EMST',
+ dep_station => 'EMSTP',
sched_departure => '16.10.2018 17:36',
rt_departure => '16.10.2018 17:36',
arr_station => 'EG',
@@ -122,7 +122,7 @@ $t->post_ok(
csrf_token => $csrf_token,
action => 'save',
train => 'RE 42 11238',
- dep_station => 'EMST',
+ dep_station => 'EMSTP',
sched_departure => '16.11.2018 17:36',
rt_departure => '16.11.2018 17:45',
arr_station => 'EG',
diff --git a/t/12-journey-edit.t b/t/12-journey-edit.t
index 27e309b..a38a7cc 100644
--- a/t/12-journey-edit.t
+++ b/t/12-journey-edit.t
@@ -16,6 +16,7 @@ use FindBin;
require "$FindBin::Bin/../index.pl";
use DateTime;
+use utf8;
my $t = Test::Mojo->new('Travelynx');
@@ -75,11 +76,12 @@ $t->post_ok(
);
$t->status_is(302)->header_is( location => '/' );
-$t->app->journeys->add(
+my ( $success, $error ) = $t->app->journeys->add(
db => $t->app->pg->db,
uid => $uid,
- dep_station => 'EMST',
- arr_station => 'EG',
+ backend_id => 1,
+ dep_station => 'Münster(Westf)Hbf',
+ arr_station => 'Gelsenkirchen Hbf',
sched_departure => DateTime->new(
year => 2018,
month => 10,
@@ -119,12 +121,22 @@ $t->app->journeys->add(
comment => 'Huhu'
);
-$t->get_ok('/journey/1')->status_is(200)->content_like(qr{M.nster\(Westf\)Hbf})
- ->content_like(qr{Gelsenkirchen Hbf})->content_like(qr{RE 11238})
- ->content_like(qr{Linie 42})->content_like(qr{..:36})
- ->content_like(qr{..:34})->content_like(qr{ca[.] 62 km})
- ->content_like(qr{Luftlinie: 62 km})->content_like(qr{64 km/h})
- ->content_like(qr{Huhu})->content_like(qr{Daten wurden manuell eingetragen});
+ok( $success, "journeys->add" );
+is( $error, undef, "journeys->add" );
+
+$t->get_ok('/journey/1')
+ ->status_is(200)
+ ->content_like(qr{M.nster\(Westf\)Hbf})
+ ->content_like(qr{Gelsenkirchen Hbf})
+ ->content_like(qr{RE 11238})
+ ->content_like(qr{Linie 42})
+ ->content_like(qr{..:36})
+ ->content_like(qr{..:34})
+ ->content_like(qr{ca[.] 62 km})
+ ->content_like(qr{Luftlinie: 62 km})
+ ->content_like(qr{64 km/h})
+ ->content_like(qr{Huhu})
+ ->content_like(qr{Daten wurden manuell eingetragen});
$t->post_ok(
'/journey/edit' => form => {
@@ -133,10 +145,14 @@ $t->post_ok(
}
);
-$t->status_is(200)->content_like(qr{M.nster\(Westf\)Hbf})
- ->content_like(qr{Gelsenkirchen Hbf})->content_like(qr{RE 11238})
- ->content_like(qr{Linie 42})->content_like(qr{16.10.2018 ..:36})
- ->content_like(qr{16.10.2018 ..:34})->content_like(qr{Huhu});
+$t->status_is(200)
+ ->content_like(qr{M.nster\(Westf\)Hbf})
+ ->content_like(qr{Gelsenkirchen Hbf})
+ ->content_like(qr{RE 11238})
+ ->content_like(qr{Linie 42})
+ ->content_like(qr{16.10.2018 ..:36})
+ ->content_like(qr{16.10.2018 ..:34})
+ ->content_like(qr{Huhu});
$csrf_token = $t->tx->res->dom->at('input[name=csrf_token]')->attr('value');
@@ -156,12 +172,19 @@ $t->post_ok(
$t->status_is(302)->header_is( location => '/journey/1' );
-$t->get_ok('/journey/1')->status_is(200)->content_like(qr{M.nster\(Westf\)Hbf})
- ->content_like(qr{Gelsenkirchen Hbf})->content_like(qr{RE 11238})
- ->content_like(qr{Linie 42})->content_like(qr{..:36})
- ->content_like(qr{..:34})->content_like(qr{ca[.] 62 km})
- ->content_like(qr{Luftlinie: 62 km})->content_like(qr{64 km/h})
- ->content_like(qr{Huhu})->content_like(qr{Daten wurden manuell eingetragen});
+$t->get_ok('/journey/1')
+ ->status_is(200)
+ ->content_like(qr{M.nster\(Westf\)Hbf})
+ ->content_like(qr{Gelsenkirchen Hbf})
+ ->content_like(qr{RE 11238})
+ ->content_like(qr{Linie 42})
+ ->content_like(qr{..:36})
+ ->content_like(qr{..:34})
+ ->content_like(qr{ca[.] 62 km})
+ ->content_like(qr{Luftlinie: 62 km})
+ ->content_like(qr{64 km/h})
+ ->content_like(qr{Huhu})
+ ->content_like(qr{Daten wurden manuell eingetragen});
$t->post_ok(
'/journey/edit' => form => {
@@ -170,10 +193,14 @@ $t->post_ok(
}
);
-$t->status_is(200)->content_like(qr{M.nster\(Westf\)Hbf})
- ->content_like(qr{Gelsenkirchen Hbf})->content_like(qr{RE 11238})
- ->content_like(qr{Linie 42})->content_like(qr{16.10.2018 ..:36})
- ->content_like(qr{16.10.2018 ..:34})->content_like(qr{Huhu});
+$t->status_is(200)
+ ->content_like(qr{M.nster\(Westf\)Hbf})
+ ->content_like(qr{Gelsenkirchen Hbf})
+ ->content_like(qr{RE 11238})
+ ->content_like(qr{Linie 42})
+ ->content_like(qr{16.10.2018 ..:36})
+ ->content_like(qr{16.10.2018 ..:34})
+ ->content_like(qr{Huhu});
$csrf_token = $t->tx->res->dom->at('input[name=csrf_token]')->attr('value');
@@ -193,13 +220,18 @@ $t->post_ok(
$t->status_is(302)->header_is( location => '/journey/1' );
-$t->get_ok('/journey/1')->status_is(200)->content_like(qr{M.nster\(Westf\)Hbf})
- ->content_like(qr{Gelsenkirchen Hbf})->content_like(qr{RE 11238})
+$t->get_ok('/journey/1')
+ ->status_is(200)
+ ->content_like(qr{M.nster\(Westf\)Hbf})
+ ->content_like(qr{Gelsenkirchen Hbf})
+ ->content_like(qr{RE 11238})
->content_like(qr{Linie 42})
- ->content_like(qr{..:42\s*\(\+6,\s*Plan: ..:36\)})
- ->content_like(qr{..:33\s*\(-1,\s*Plan: ..:34\)})
- ->content_like(qr{ca[.] 62 km})->content_like(qr{Luftlinie: 62 km})
- ->content_like(qr{73 km/h})->content_like(qr{Huhu})
+ ->content_like(qr{..:42\s*\n*\s*\(\+6,\s*Plan: ..:36\)})
+ ->content_like(qr{..:33\s*\n*\s*\(-1,\s*Plan: ..:34\)})
+ ->content_like(qr{ca[.] 62 km})
+ ->content_like(qr{Luftlinie: 62 km})
+ ->content_like(qr{73 km/h})
+ ->content_like(qr{Huhu})
->content_like(qr{Daten wurden manuell eingetragen});
$t->app->pg->db->query('drop schema travelynx_test_12 cascade');
diff --git a/t/22-transit-visibility.t b/t/22-transit-visibility.t
index 7e995c5..8a68f5c 100644
--- a/t/22-transit-visibility.t
+++ b/t/22-transit-visibility.t
@@ -108,10 +108,10 @@ sub test_intransit_visibility {
$opt{effective_visibility_str}, $desc );
if ( $opt{public} ) {
- $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN 667});
+ $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN\s*667});
$t->get_ok('/ajax/status/test1.html')->status_is(200)
- ->content_like(qr{DPN 667});
- $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
+ $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN\s*667});
}
else {
$t->get_ok('/status/test1')->status_is(200)
@@ -124,9 +124,9 @@ sub test_intransit_visibility {
if ( $opt{with_token} ) {
$t->get_ok("/status/test1/$token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
$t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/status/test1/$token")->status_is(200)
@@ -142,10 +142,10 @@ sub test_intransit_visibility {
# users can see their own status if visibility is >= followrs
if ( $opt{effective_visibility} >= 60 ) {
- $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN 667});
+ $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN\s*667});
$t->get_ok('/ajax/status/test1.html')->status_is(200)
- ->content_like(qr{DPN 667});
- $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
+ $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN\s*667});
}
else {
$t->get_ok('/status/test1')->status_is(200)
@@ -159,9 +159,9 @@ sub test_intransit_visibility {
# users can see their own status with token if visibility is >= unlisted
if ( $opt{effective_visibility} >= 30 ) {
$t->get_ok("/status/test1/$token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
$t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/status/test1/$token")->status_is(200)
@@ -178,10 +178,10 @@ sub test_intransit_visibility {
# uid2 can see uid1 if visibility is >= followers
if ( $opt{effective_visibility} >= 60 ) {
- $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN 667});
+ $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN\s*667});
$t->get_ok('/ajax/status/test1.html')->status_is(200)
- ->content_like(qr{DPN 667});
- $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
+ $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN\s*667});
}
else {
$t->get_ok('/status/test1')->status_is(200)
@@ -195,9 +195,9 @@ sub test_intransit_visibility {
# uid2 can see uid1 with token if visibility is >= unlisted
if ( $opt{effective_visibility} >= 30 ) {
$t->get_ok("/status/test1/$token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
$t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/status/test1/$token")->status_is(200)
@@ -214,10 +214,10 @@ sub test_intransit_visibility {
# uid3 can see uid1 if visibility is >= travelynx
if ( $opt{effective_visibility} >= 80 ) {
- $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN 667});
+ $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN\s*667});
$t->get_ok('/ajax/status/test1.html')->status_is(200)
- ->content_like(qr{DPN 667});
- $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
+ $t->get_ok('/p/test1')->status_is(200)->content_like(qr{DPN\s*667});
}
else {
$t->get_ok('/status/test1')->status_is(200)
@@ -231,9 +231,9 @@ sub test_intransit_visibility {
# uid3 can see uid1 with token if visibility is >= unlisted
if ( $opt{effective_visibility} >= 30 ) {
$t->get_ok("/status/test1/$token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
$t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/status/test1/$token")->status_is(200)
@@ -341,6 +341,7 @@ $t->app->in_transit->add(
departure_eva => 8000001,
train => $train_dep,
route => [],
+ backend_id => $t->app->stations->get_backend_id( iris => 1 ),
);
$t->app->in_transit->set_arrival_eva(
uid => $uid1,
diff --git a/t/23-journey-visibility.t b/t/23-journey-visibility.t
index 2124940..1cc7e64 100644
--- a/t/23-journey-visibility.t
+++ b/t/23-journey-visibility.t
@@ -111,7 +111,7 @@ sub test_journey_visibility {
if ( $opt{public} ) {
$t->get_ok("/p/test1/j/$jid")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/p/test1/j/$jid")->status_is(404)
@@ -120,7 +120,7 @@ sub test_journey_visibility {
if ( $opt{with_token} ) {
$t->get_ok("/p/test1/j/$jid$token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/p/test1/j/$jid$token")->status_is(404)
@@ -135,7 +135,7 @@ sub test_journey_visibility {
# users can see their own status if visibility is >= followrs
if ( $opt{effective_visibility} >= 60 ) {
$t->get_ok("/p/test1/j/$jid")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/p/test1/j/$jid")->status_is(404)
@@ -145,7 +145,7 @@ sub test_journey_visibility {
# users can see their own status with token if visibility is >= unlisted
if ( $opt{effective_visibility} >= 30 ) {
$t->get_ok("/p/test1/j/$jid$token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/p/test1/j/$jid$token")->status_is(404)
@@ -161,7 +161,7 @@ sub test_journey_visibility {
# uid2 can see uid1 if visibility is >= followers
if ( $opt{effective_visibility} >= 60 ) {
$t->get_ok("/p/test1/j/$jid")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/p/test1/j/$jid")->status_is(404)
@@ -171,7 +171,7 @@ sub test_journey_visibility {
# uid2 can see uid1 with token if visibility is >= unlisted
if ( $opt{effective_visibility} >= 30 ) {
$t->get_ok("/p/test1/j/$jid$token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/p/test1/j/$jid$token")->status_is(404)
@@ -187,7 +187,7 @@ sub test_journey_visibility {
# uid3 can see uid1 if visibility is >= travelynx
if ( $opt{effective_visibility} >= 80 ) {
$t->get_ok("/p/test1/j/$jid")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/p/test1/j/$jid")->status_is(404)
@@ -197,7 +197,7 @@ sub test_journey_visibility {
# uid3 can see uid1 with token if visibility is >= unlisted
if ( $opt{effective_visibility} >= 30 ) {
$t->get_ok("/p/test1/j/$jid$token")->status_is(200)
- ->content_like(qr{DPN 667});
+ ->content_like(qr{DPN\s*667});
}
else {
$t->get_ok("/p/test1/j/$jid$token")->status_is(404)
@@ -303,6 +303,7 @@ $t->app->in_transit->add(
departure_eva => 8000001,
train => $train_dep,
route => [],
+ backend_id => $t->app->stations->get_backend_id( iris => 1 ),
);
$t->app->in_transit->set_arrival_eva(
uid => $uid1,
diff --git a/t/24-past-visibility.t b/t/24-past-visibility.t
index 51c8081..cf981b9 100644
--- a/t/24-past-visibility.t
+++ b/t/24-past-visibility.t
@@ -116,11 +116,11 @@ sub test_history_visibility {
if ( $opt{public} ) {
$t->get_ok('/p/test1')->status_is(200)
- ->content_like( qr{DPN 667}, "public $desc" );
+ ->content_like( qr{DPN\s*667}, "public $desc" );
}
else {
$t->get_ok('/p/test1')->status_is(200)
- ->content_unlike( qr{DPN 667}, "public $desc" );
+ ->content_unlike( qr{DPN\s*667}, "public $desc" );
}
login(
@@ -130,11 +130,11 @@ sub test_history_visibility {
if ( $opt{self} ) {
$t->get_ok('/p/test1')->status_is(200)
- ->content_like( qr{DPN 667}, "self $desc" );
+ ->content_like( qr{DPN\s*667}, "self $desc" );
}
else {
$t->get_ok('/p/test1')->status_is(200)
- ->content_unlike( qr{DPN 667}, "self $desc" );
+ ->content_unlike( qr{DPN\s*667}, "self $desc" );
}
logout();
@@ -145,11 +145,11 @@ sub test_history_visibility {
if ( $opt{followers} ) {
$t->get_ok('/p/test1')->status_is(200)
- ->content_like( qr{DPN 667}, "follower $desc" );
+ ->content_like( qr{DPN\s*667}, "follower $desc" );
}
else {
$t->get_ok('/p/test1')->status_is(200)
- ->content_unlike( qr{DPN 667}, "follower $desc" );
+ ->content_unlike( qr{DPN\s*667}, "follower $desc" );
}
logout();
@@ -160,11 +160,11 @@ sub test_history_visibility {
if ( $opt{travelynx} ) {
$t->get_ok('/p/test1')->status_is(200)
- ->content_like( qr{DPN 667}, "travelynx $desc" );
+ ->content_like( qr{DPN\s*667}, "travelynx $desc" );
}
else {
$t->get_ok('/p/test1')->status_is(200)
- ->content_unlike( qr{DPN 667}, "travelynx $desc" );
+ ->content_unlike( qr{DPN\s*667}, "travelynx $desc" );
}
logout();
@@ -266,6 +266,7 @@ $t->app->in_transit->add(
departure_eva => 8000001,
train => $train_dep,
route => [],
+ backend_id => $t->app->stations->get_backend_id( iris => 1 ),
);
$t->app->in_transit->set_arrival_eva(
uid => $uid1,
diff --git a/t/r-negative-delay.t b/t/r-negative-delay.t
index 78bd6e0..4f9d94e 100644
--- a/t/r-negative-delay.t
+++ b/t/r-negative-delay.t
@@ -82,7 +82,7 @@ $t->post_ok(
csrf_token => $csrf_token,
action => 'save',
train => 'RE 42 11238',
- dep_station => 'EMST',
+ dep_station => 'EMSTP',
sched_departure => '16.10.2018 17:36',
rt_departure => '16.10.2018 17:35',
arr_station => 'EG',
diff --git a/templates/_backend_line.html.ep b/templates/_backend_line.html.ep
new file mode 100644
index 0000000..00496d3
--- /dev/null
+++ b/templates/_backend_line.html.ep
@@ -0,0 +1,25 @@
+<div class="row">
+ <div class="col s8 m6 l6 right-align">
+ %= $backend->{longname}
+ % if ($backend->{id} == $user->{backend_id}) {
+ (aktuell ausgewählt)
+ % }
+ % if ($backend->{has_area}) {
+ <br/>
+ <a href="https://dbf.finalrewind.org/coverage/<%= $backend->{type} %>/<%= $backend->{name} %>"><%= join(q{, }, @{$backend->{regions} // []}) || '[Karte]' %></a>
+ % }
+ % elsif ($backend->{regions}) {
+ <br/>
+ %= join(q{, }, @{$backend->{regions} // []})
+ % }
+ % if ($backend->{homepage}) {
+ <br/>
+ <a href="<%= $backend->{homepage} %>"><%= $backend->{homepage} =~ s{ ^ http s? :// (?: www[.] )? (.*?) (?: / )? $ }{$1}xr %></a>
+ % }
+ </div>
+ <div class="col s4 m6 l6 left-align">
+ <button class="btn waves-effect waves-light <%= $backend->{id} == $user->{backend_id} ? 'disabled' : q{} %>" style="min-width: 6em;" type="submit" name="backend" value="<%= $backend->{id} %>">
+ <%= $backend->{name} %>
+ </button>
+ </div>
+</div>
diff --git a/templates/_checked_in.html.ep b/templates/_checked_in.html.ep
index 7155208..91f1ce7 100644
--- a/templates/_checked_in.html.ep
+++ b/templates/_checked_in.html.ep
@@ -42,13 +42,13 @@
% }
% if ($journey->{arrival_countdown} < (60 * 15) and $journey->{arr_platform}) {
% if ($journey->{arr_direction} and $journey->{arr_direction} eq 'r') {
- <br/>Gleis <%= $journey->{arr_platform} %> ▶
+ <br/><%= $journey->{platform_type} %> <%= $journey->{arr_platform} %> ▶
% }
% elsif ($journey->{arr_direction} and $journey->{arr_direction} eq 'l') {
- <br/>◀ Gleis <%= $journey->{arr_platform} %>
+ <br/>◀ <%= $journey->{platform_type} %> <%= $journey->{arr_platform} %>
% }
% else {
- <br/>auf Gleis <%= $journey->{arr_platform} %>
+ <br/>auf <%= $journey->{platform_type} %> <%= $journey->{arr_platform} %>
% }
% }
% }
@@ -57,43 +57,60 @@
% }
% if ($journey->{departure_countdown} > 0 and $journey->{dep_platform}) {
% if ($journey->{dep_direction} and $journey->{dep_direction} eq 'r') {
- <br/>Gleis <%= $journey->{dep_platform} %> ▶
+ <br/><%= $journey->{platform_type} %> <%= $journey->{dep_platform} %> ▶
% }
% elsif ($journey->{dep_direction} and $journey->{dep_direction} eq 'l') {
- <br/>◀ Gleis <%= $journey->{dep_platform} %>
+ <br/>◀ <%= $journey->{platform_type} %> <%= $journey->{dep_platform} %>
% }
% else {
- <br/>von Gleis <%= $journey->{dep_platform} %>
+ <br/>von <%= $journey->{platform_type} %> <%= $journey->{dep_platform} %>
% }
% }
% if (my $wr = $journey->{wagonorder}) {
<br/>
- % my @wagons = $wr->wagons;
- % my $direction = $wr->direction == 100 ? '→' : '←';
- % if ($journey->{dep_direction}) {
- % $direction = $journey->{dep_direction} eq 'l' ? '◀' : '▶';
- % if (($journey->{dep_direction} eq 'l' ? 0 : 100) != $wr->direction) {
- % @wagons = reverse @wagons;
+ <a href="https://dbf.finalrewind.org/carriage-formation?<%= join('&', map { $_ . '=' . $journey->{extra_data}{wagonorder_param}{$_} } sort keys %{$journey->{extra_data}{wagonorder_param}}) %>&amp;e=<%= $journey->{dep_direction} // q{} %>">
+ % my $direction = $wr->direction == 100 ? '→' : '←';
+ % my $rev = 0;
+ % if ($journey->{dep_direction}) {
+ % $direction = $journey->{dep_direction} eq 'l' ? '◀' : '▶';
+ % $rev = (($journey->{dep_direction} eq 'l' ? 0 : 100) == $wr->direction) ? 0 : 1;
% }
- % }
- <a href="https://dbf.finalrewind.org/_wr/<%= $journey->{train_no} %>/<%= $journey->{sched_departure}->strftime('%Y%m%d%H%M') %>?e=<%= $journey->{dep_direction} // q{} %>">
- %= $direction
- % my $gi;
- % for my $wagon (@wagons) {
- % if (not ($wagon->is_locomotive or $wagon->is_powercar)) {
- % if (defined $gi and $gi != $wagon->group_index) {
+ %= $direction
+ % my $had_entry = 0;
+ % for my $group ($rev ? reverse $wr->groups : $wr->groups) {
+ % if ($had_entry) {
+ % $had_entry = 0;
% }
- % if ($wagon->is_closed) {
- X
- % }
- % else {
- %= $wagon->number || ($wagon->type =~ m{AB} ? '½' : $wagon->type =~ m{A} ? '1.' : $wagon->type =~ m{B} ? '2.' : $wagon->type )
+ % for my $wagon ($rev ? reverse $group->carriages : $group->carriages) {
+ % if (not ($wagon->is_locomotive or $wagon->is_powercar)) {
+ % $had_entry = 1;
+ % if ($wagon->is_closed) {
+ X
+ % }
+ % elsif ( $wagon->number) {
+ %= $wagon->number
+ % }
+ % else {
+ % if ( $wagon->has_first_class ) {
+ % if ( $wagon->has_second_class ) {
+ ½
+ % }
+ % else {
+ 1.
+ % }
+ % }
+ % elsif ( $wagon->has_second_class ) {
+ 2.
+ % }
+ % else {
+ %= $wagon->type;
+ % }
+ % }
+ % }
% }
% }
- % $gi = $wagon->group_index;
- % }
- %= $direction
+ %= $direction
</a>
% }
</div>
@@ -111,12 +128,7 @@
% }
</div>
<div style="float: right; text-align: right;">
- % if ($user->{sb_template}) {
- <b><a href="<%= resolve_sb_template($user->{sb_template}, name => $journey->{arr_name}, eva => $journey->{arr_eva}, tt => $journey->{train_type} // q{x}, tn => $journey->{train_no}, id => $journey->{train_id}) %>" class="unmarked"><%= $journey->{arr_name} %></a></b><br/>
- % }
- % else {
- <b><%= $journey->{arr_name} %></b><br/>
- % }
+ <b><a href="<%= resolve_sb_template($user->{sb_template}, name => $journey->{arr_name}, eva => $journey->{arr_eva}, tt => $journey->{train_type} // q{x}, tn => $journey->{train_no}, id => $journey->{train_id} =~ s{[ #|]}{x}gr, dbris => $journey->{is_dbris} ? $journey->{backend_name} : q{}, efa => $journey->{is_efa} ? $journey->{backend_name} : q{}, hafas => $journey->{is_hafas} ? $journey->{backend_name} : q{}, is_iris => $journey->{is_iris}, motis => $journey->{is_motis} ? $journey->{backend_name} : q{}) %>" class="unmarked"><%= $journey->{arr_name} %></a></b><br/>
% if ($journey->{real_arrival}->epoch) {
<b><%= $journey->{real_arrival}->strftime('%H:%M') %></b>
% if ($journey->{real_arrival}->epoch != $journey->{sched_arrival}->epoch) {
@@ -190,7 +202,10 @@
% if (@{$journey->{messages} // []} or @{$journey->{extra_data}{qos_msg} // []} or not $journey->{extra_data}{rt}) {
<p style="margin-bottom: 2ex;">
<ul>
- % if (not $journey->{extra_data}{rt}) {
+ % if ($journey->{extra_data}{manual}) {
+ <li><i class="material-icons tiny">gps_off</i> Manueller Checkin ohne Echtzeitdaten
+ % }
+ % elsif (not $journey->{extra_data}{rt}) {
<li><i class="material-icons tiny">gps_off</i> Keine Echtzeitdaten vorhanden
% }
% for my $message (reverse @{$journey->{messages} // []}) {
@@ -230,10 +245,7 @@
<a class="tablerow action-checkout" data-station="<%= $station->[1] // $station->[0] %>">
<span><%= $station->[0] %></span>
<span>
- % if ($station->[2]{load}{SECOND}) {
- % my ($first, $second) = load_icon($station->[2]{load});
- <i class="material-icons tiny" aria-hidden="true"><%= $first %></i> <i class="material-icons tiny" aria-hidden="true"><%= $second %></i>
- % }
+ %= include '_show_load_icons', station => $station
% if ($station->[2]{isCancelled}) {
entfällt
% }
@@ -312,15 +324,16 @@
% if (@{stash('timeline') // []}) {
%= include '_timeline_link', timeline => stash('timeline'), from_checkin => 1
% }
- % if ($journey->{arr_name}) {
+ % if ($journey->{arr_name} and @{$journey->{extra_data}{him_msg} // []}) {
<div class="card" style="margin-top: <%= scalar @{stash('timeline') // []} ? '1.5rem' : '3em' %>;">
<div class="card-content">
- <span class="card-title">Details</span>
+ <i class="material-icons small right sync-failed-marker grey-text" style="display: none;">sync_problem</i>
+ <span class="card-title">Meldungen</span>
% if (@{$journey->{extra_data}{him_msg} // []}) {
<p style="margin-bottom: 2ex;">
<ul>
% for my $message (@{$journey->{extra_data}{him_msg} // []}) {
- <li> <i class="material-icons tiny">info</i> <%= $message->{header} %> <%= $message->{lead} %></li>
+ <li> <i class="material-icons tiny"><%= ($message->{prio} and $message->{prio} eq 'HOCH') ? 'warning' : 'info' %></i> <%= $message->{header} %> <%= $message->{lead} %></li>
% }
</ul>
</p>
@@ -340,70 +353,68 @@
</p>
% }
</div>
- <div class="card-action">
- % my $url = 'https://bahn.expert/details/';
- % if ($journey->{train_id} =~ m{[|]}) {
- % $url = $url . '/' . $journey->{sched_departure}->epoch . '000?jid=' . $journey->{train_id};
- % }
- % else {
- % $url = $url . $journey->{train_type} . ' ' . $journey->{train_no} . '/' . $journey->{sched_departure}->epoch . '000?station=' . $journey->{dep_eva};
- % }
- <a style="margin-right: 0;" href="<%= $url %>"><i class="material-icons left" aria-hidden="true">timeline</i> Zuglauf</a>
- % if ($journey->{extra_data}{trip_id}) {
- <a class="right" style="margin-right: 0;" href="https://dbf.finalrewind.org/map/<%= $journey->{extra_data}{trip_id} %>/<%= $journey->{train_line} || 0 %>?from=<%= $journey->{dep_name} %>&amp;to=<%= $journey->{arr_name} %>&amp;dark=<%= (session('theme') and session('theme') eq 'dark') ? 1 : 0 %>"><i class="material-icons left" aria-hidden="true">map</i> Karte</a>
- % }
- </div>
</div>
+ % }
+ % if ($journey->{arr_name}) {
<div class="card" style="margin-top: 3em;">
<div class="card-content">
<i class="material-icons small right sync-failed-marker grey-text" style="display: none;">sync_problem</i>
- <span class="card-title">Ziel ändern?</span>
- % if ($user->{sb_template}) {
- <div class="targetlist">
- % }
- % else {
- <p>
- % }
- % for my $station (@{$journey->{route_after}}) {
- % my $is_dest = ($journey->{arr_name} and $station->[0] eq $journey->{arr_name});
- <a class="action-checkout tablerow" style="<%= $is_dest? 'font-weight: bold;' : '' %>" data-station="<%= $station->[1] // $station->[0] %>">
- <span><%= $station->[0] %></span>
- <span>
- % if ($station->[2]{load}{SECOND}) {
- % my ($first, $second) = load_icon($station->[2]{load});
- <i class="material-icons tiny" aria-hidden="true"><%= $first %></i> <i class="material-icons tiny" aria-hidden="true"><%= $second %></i>
- % }
- % if ($station->[2]{isCancelled}) {
- entfällt
- % }
- % elsif ($station->[2]{rt_arr} or $station->[2]{sched_arr}) {
- %= ($station->[2]{rt_arr} || $station->[2]{sched_arr})->strftime('%H:%M')
- % }
- % elsif ($station->[2]{rt_dep} or $station->[2]{sched_dep}) {
- (<%= ($station->[2]{rt_dep} || $station->[2]{sched_dep})->strftime('%H:%M') %>)
- % }
- % elsif ($station->[2]{isAdditional}) {
- Zusatzhalt
- % }
- </span>
+ <span class="card-title">Karte</span>
+ <div id="map" style="height: 70vh;">
+ </div>
+ %= include '_map', with_map_header => 0, station_coordinates => stash('station_coordinates'), polyline_groups => stash('polyline_groups')
+ </div>
+ </div>
+ % if ($journey->{extra_data}{manual}) {
+ <div class="card" style="margin-top: 3em;">
+ <div class="card-content">
+ <i class="material-icons small right sync-failed-marker grey-text" style="display: none;">sync_problem</i>
+ <span class="card-title">Manueller Checkin</span>
+ </div>
+ <div class="card-action">
+ <a class="action-undo blue-text" data-id="in_transit" data-checkints="<%= $journey->{timestamp}->epoch %>" style="margin-right: 0;">
+ <i class="material-icons left" aria-hidden="true">undo</i> Checkin Rückgängig
</a>
- % if ($user->{sb_template}) {
- <a class="nonflex" href="<%= resolve_sb_template($user->{sb_template}, name => $station->[0], eva => $station->[1], tt => $journey->{train_type} // q{x}, tn => $journey->{train_no}, id => $journey->{train_id}) %>"><i class="material-icons tiny">train</i></a>
+ </div>
+ </div>
+ % }
+ % else {
+ <div class="card" style="margin-top: 3em;">
+ <div class="card-content">
+ <i class="material-icons small right sync-failed-marker grey-text" style="display: none;">sync_problem</i>
+ <span class="card-title">Ziel ändern?</span>
+ <div class="targetlist">
+ % for my $station (@{$journey->{route_after}}) {
+ % my $is_dest = ($journey->{arr_name} and $station->[0] eq $journey->{arr_name});
+ <a class="action-checkout tablerow" style="<%= $is_dest? 'font-weight: bold;' : '' %>" data-station="<%= $station->[1] // $station->[0] %>">
+ <span><%= $station->[0] %></span>
+ <span>
+ %= include '_show_load_icons', station => $station
+ % if ($station->[2]{isCancelled}) {
+ entfällt
+ % }
+ % elsif ($station->[2]{rt_arr} or $station->[2]{sched_arr}) {
+ %= ($station->[2]{rt_arr} || $station->[2]{sched_arr})->strftime('%H:%M')
+ % }
+ % elsif ($station->[2]{rt_dep} or $station->[2]{sched_dep}) {
+ (<%= ($station->[2]{rt_dep} || $station->[2]{sched_dep})->strftime('%H:%M') %>)
+ % }
+ % elsif ($station->[2]{isAdditional}) {
+ Zusatzhalt
+ % }
+ </span>
+ </a>
+ <a class="nonflex" href="<%= resolve_sb_template($user->{sb_template}, name => $station->[0], eva => $station->[1], tt => $journey->{train_type} // q{x}, tn => $journey->{train_no}, id => $journey->{train_id} =~ s{[ #|]}{x}gr, dbris => $journey->{is_dbris} ? $journey->{backend_name} : q{}, efa => $journey->{is_efa} ? $journey->{backend_name} : q{}, hafas => $journey->{is_hafas} ? $journey->{backend_name} : q{}, is_iris => $journey->{is_iris}, motis => $journey->{is_motis} ? $journey->{backend_name} : q{}) %>"><i class="material-icons tiny"><%= $journey->{is_hafas} ? 'directions' : 'train' %></i></a>
% }
- % }
- % if ($user->{sb_template}) {
</div>
- % }
- % else {
- </p>
- % }
- </div>
- <div class="card-action">
- <a class="action-undo blue-text" data-id="in_transit" data-checkints="<%= $journey->{timestamp}->epoch %>" style="margin-right: 0;">
- <i class="material-icons left" aria-hidden="true">undo</i> Checkin Rückgängig
- </a>
+ </div>
+ <div class="card-action">
+ <a class="action-undo blue-text" data-id="in_transit" data-checkints="<%= $journey->{timestamp}->epoch %>" style="margin-right: 0;">
+ <i class="material-icons left" aria-hidden="true">undo</i> Checkin Rückgängig
+ </a>
+ </div>
</div>
- </div>
+ % }
<p>
Falls das Backend ausgefallen ist oder die Fahrt aus anderen
Gründen verloren ging:
diff --git a/templates/_checked_out.html.ep b/templates/_checked_out.html.ep
index 5a944dc..21db335 100644
--- a/templates/_checked_out.html.ep
+++ b/templates/_checked_out.html.ep
@@ -3,7 +3,7 @@
<span class="card-title">Ausgecheckt</span>
<p>Aus
%= include '_format_train', journey => $journey
- bis <a href="/s/<%= $journey->{arr_eva} %>?hafas=<%= $journey->{train_id} =~ m{[|]} ? 1 : 0 %>"><%= $journey->{arr_name} %></a></p>
+ bis <a href="/s/<%= $journey->{arr_eva} %>?hafas=<%= $journey->{is_hafas} ? $journey->{backend_name} : q{} %>"><%= $journey->{arr_name} %></a></p>
% if (@{stash('connections_iris') // [] } or @{stash('connections_hafas') // []}) {
<span class="card-title" style="margin-top: 2ex;">Verbindungen</span>
<p>Fahrt auswählen zum Einchecken mit Zielwahl.</p>
diff --git a/templates/_connections_hafas.html.ep b/templates/_connections_hafas.html.ep
index dcf7ec9..3b995b5 100644
--- a/templates/_connections_hafas.html.ep
+++ b/templates/_connections_hafas.html.ep
@@ -1,6 +1,6 @@
<ul class="collection departures connections">
% for my $res (@{$connections}) {
- % my ($train, $via, $via_arr) = @{$res};
+ % my ($train, $via, $via_arr, $hafas_service) = @{$res};
% $via_arr = $via_arr ? $via_arr->strftime('%H:%M') : q{};
% my $row_class = '';
% my $link_class = 'action-checkin';
@@ -10,6 +10,7 @@
% }
% if ($checkin_from) {
<li class="collection-item <%= $row_class %> <%= $link_class %>"
+ data-hafas="<%= $hafas_service %>"
data-station="<%= $train->station_eva %>"
data-train="<%= $train->id %>"
data-ts="<%= ($train->sched_datetime // $train->datetime)->epoch %>"
@@ -34,7 +35,15 @@
</a>
<span class="connect-platform-wrapper">
% if ($train->platform) {
- <span>Gleis <%= $train->platform %></span>
+ <span>
+ % if (($train->type // q{}) =~ m{ ast | bus | ruf }ix) {
+ Steig
+ % }
+ % else {
+ Gleis
+ % }
+ %= $train->platform
+ </span>
% }
<span class="dep-line <%= $train->type // q{} %>">
%= $train->line
diff --git a/templates/_departures_dbris.html.ep b/templates/_departures_dbris.html.ep
new file mode 100644
index 0000000..dbd1a70
--- /dev/null
+++ b/templates/_departures_dbris.html.ep
@@ -0,0 +1,55 @@
+<ul class="collection departures">
+% my $orientation_bar_shown = param('train');
+% my $now_epoch = now->epoch;
+% for my $result (@{$results}) {
+ % my $row_class = '';
+ % my $link_class = 'action-checkin';
+ % if ($result->is_cancelled) {
+ % $row_class = "cancelled";
+ % $link_class = 'action-cancelled-from';
+ % }
+ % if (not $orientation_bar_shown and $result->dep->epoch < $now_epoch) {
+ % $orientation_bar_shown = 1;
+ <li class="collection-item" id="now">
+ <strong class="dep-time">
+ %= now->strftime('%H:%M')
+ </strong>
+ <strong>— Anfragezeitpunkt —</strong>
+ </li>
+ % }
+ <li class="collection-item <%= $link_class %> <%= $row_class %>"
+ data-dbris="<%= $dbris %>"
+ data-station="<%= $result->stop_eva %>"
+ data-train="<%= $result->id %>"
+ data-suffix="<%= $result->maybe_line_no %>"
+ data-ts="<%= ($result->sched_dep // $result->dep)->epoch %>"
+ >
+ <a class="dep-time" href="#">
+ %= $result->dep->strftime('%H:%M')
+ % if ($result->delay) {
+ (<%= sprintf('%+d', $result->delay) %>)
+ % }
+ % elsif (not defined $result->delay and not $result->is_cancelled) {
+ <i class="material-icons" aria-label="Keine Echtzeitdaten vorhanden" style="font-size: 16px;">gps_off</i>
+ % }
+ </a>
+ <span class="dep-line <%= $result->type // q{} %>">
+ %= $result->line
+ </span>
+ <span class="dep-dest">
+ % if ($result->is_cancelled) {
+ Fahrt nach <%= $result->destination // $result->via_last %> entfällt
+ % }
+ % else {
+ %= $result->destination // $result->via_last
+ % for my $checkin (@{$checkin_by_train->{$result->id} // []}) {
+ <span class="followee-checkin">
+ <i class="material-icons tiny" aria-label="Eine Person, der du folgst, ist hier eingecheckt">people</i>
+ <%= $checkin->{followee_name} %> → <%= $checkin->{arr_name} // '???' %>
+ </span>
+ % }
+ % }
+ </span>
+ </li>
+% }
+</ul>
diff --git a/templates/_departures_efa.html.ep b/templates/_departures_efa.html.ep
new file mode 100644
index 0000000..26af13f
--- /dev/null
+++ b/templates/_departures_efa.html.ep
@@ -0,0 +1,57 @@
+<ul class="collection departures">
+% my $orientation_bar_shown = param('train');
+% my $now_epoch = now->epoch;
+% for my $result (@{$results}) {
+ % my $row_class = '';
+ % my $link_class = 'action-checkin';
+ % if ($result->is_cancelled) {
+ % $row_class = "cancelled";
+ % $link_class = 'action-cancelled-from';
+ % }
+ % if (not $orientation_bar_shown and $result->datetime->epoch < $now_epoch) {
+ % $orientation_bar_shown = 1;
+ <li class="collection-item" id="now">
+ <strong class="dep-time">
+ %= now->strftime('%H:%M')
+ </strong>
+ <strong>— Anfragezeitpunkt —</strong>
+ </li>
+ % }
+ <li class="collection-item <%= $link_class %> <%= $row_class %>"
+ data-efa="<%= $efa %>"
+ data-station="<%= $result->stop_id_num %>"
+ data-train="<%= $result->id %>"
+ data-ts="<%= ($result->sched_datetime // $result->datetime)->epoch %>"
+ >
+ <a class="dep-time" href="#">
+ %= $result->datetime->strftime('%H:%M')
+ % if ($result->delay) {
+ (<%= sprintf('%+d', $result->delay) %>)
+ % }
+ % elsif (not defined $result->delay and not $result->is_cancelled) {
+ <i class="material-icons" aria-label="Keine Echtzeitdaten vorhanden" style="font-size: 16px;">gps_off</i>
+ % }
+ </a>
+ <span class="dep-line <%= ($result->type // q{}) =~ tr{a-zA-Z_-}{}cdr %>">
+ %= $result->line
+ </span>
+ <span class="dep-dest">
+ % if ($result->is_cancelled) {
+ Fahrt nach <%= $result->destination %> entfällt
+ % }
+ % else {
+ %= $result->destination
+ % for my $checkin (@{$checkin_by_train->{$result->id} // []}) {
+ <span class="followee-checkin">
+ <i class="material-icons tiny" aria-label="Eine Person, der du folgst, ist hier eingecheckt">people</i>
+ <%= $checkin->{followee_name} %> → <%= $checkin->{arr_name} // '???' %>
+ </span>
+ % }
+ % if ($result->occupancy) {
+ <i class="material-icons tiny" aria-hidden="true"><%= efa_load_icon($result->occupancy) %></i>
+ % }
+ % }
+ </span>
+ </li>
+% }
+</ul>
diff --git a/templates/_departures_hafas.html.ep b/templates/_departures_hafas.html.ep
index 9e4d7a4..5825ba0 100644
--- a/templates/_departures_hafas.html.ep
+++ b/templates/_departures_hafas.html.ep
@@ -18,6 +18,7 @@
</li>
% }
<li class="collection-item <%= $link_class %> <%= $row_class %>"
+ data-hafas="<%= $hafas %>"
data-station="<%= $result->station_eva %>"
data-train="<%= $result->id %>"
data-ts="<%= ($result->sched_datetime // $result->datetime)->epoch %>"
@@ -40,6 +41,13 @@
% }
% else {
%= $result->destination
+ % if ($result->load and $result->load->{SECOND}) {
+ % my ($first, $second) = load_icon($result->load);
+ % if ($first ne 'help_outline') {
+ <i class="material-icons tiny" aria-hidden="true"><%= $first %></i>
+ % }
+ <i class="material-icons tiny" aria-hidden="true"><%= $second %></i>
+ % }
% for my $checkin (@{$checkin_by_train->{$result->id} // []}) {
<span class="followee-checkin">
<i class="material-icons tiny" aria-label="Eine Person, der du folgst, ist hier eingecheckt">people</i>
diff --git a/templates/_departures_motis.html.ep b/templates/_departures_motis.html.ep
new file mode 100644
index 0000000..2ebc5de
--- /dev/null
+++ b/templates/_departures_motis.html.ep
@@ -0,0 +1,54 @@
+<ul class="collection departures">
+% my $orientation_bar_shown = param('train');
+% my $now_epoch = now->epoch;
+% for my $result (@{$results}) {
+ % my $row_class = '';
+ % my $link_class = 'action-checkin';
+ % if ($result->is_cancelled) {
+ % $row_class = "cancelled";
+ % $link_class = 'action-cancelled-from';
+ % }
+ % if (not $orientation_bar_shown and $result->stopover->departure->epoch < $now_epoch) {
+ % $orientation_bar_shown = 1;
+ <li class="collection-item" id="now">
+ <strong class="dep-time">
+ %= now->strftime('%H:%M')
+ </strong>
+ <strong>— Anfragezeitpunkt —</strong>
+ </li>
+ % }
+ <li class="collection-item <%= $link_class %> <%= $row_class %>"
+ data-motis="<%= $motis %>"
+ data-station="<%= $result->stopover->stop->id %>"
+ data-train="<%= $result->id %>"
+ data-ts="<%= ($result->stopover->departure)->epoch %>"
+ >
+ <a class="dep-time" href="#">
+ %= $result->stopover->departure->strftime('%H:%M')
+ % if ($result->stopover->delay) {
+ (<%= sprintf('%+d', $result->stopover->delay) %>)
+ % }
+ % elsif (not $result->stopover->is_realtime and not $result->stopover->is_cancelled) {
+ <i class="material-icons" aria-label="Keine Echtzeitdaten vorhanden" style="font-size: 16px;">gps_off</i>
+ % }
+ </a>
+ <span class="dep-line <%= $result->mode %>" style="background-color: #<%= $result->route_color // q{} %>;">
+ %= $result->route_name
+ </span>
+ <span class="dep-dest">
+ % if ($result->is_cancelled) {
+ Fahrt nach <%= $result->headsign %> entfällt
+ % }
+ % else {
+ %= $result->headsign
+ % for my $checkin (@{$checkin_by_train->{$result->id} // []}) {
+ <span class="followee-checkin">
+ <i class="material-icons tiny" aria-label="Eine Person, der du folgst, ist hier eingecheckt">people</i>
+ <%= $checkin->{followee_name} %> → <%= $checkin->{arr_name} // '???' %>
+ </span>
+ % }
+ % }
+ </span>
+ </li>
+% }
+</ul>
diff --git a/templates/_format_train.html.ep b/templates/_format_train.html.ep
index 1d6acaa..cb81211 100644
--- a/templates/_format_train.html.ep
+++ b/templates/_format_train.html.ep
@@ -1,8 +1,10 @@
% if ($journey->{extra_data}{wagonorder_pride}) {
🏳️‍🌈
% }
-<span class="dep-line <%= $journey->{train_type} // q{} %>">
- <%= $journey->{train_type} %>
+<span class="dep-line <%= ($journey->{train_type} // q{}) =~ tr{a-zA-Z_-}{}cdr %>">
+ % if (not $journey->{is_motis}) {
+ <%= $journey->{train_type} %>
+ % }
<%= $journey->{train_line} // $journey->{train_no}%>
</span>
% if ($journey->{train_line}) {
diff --git a/templates/_history_trains.html.ep b/templates/_history_trains.html.ep
index cf998ab..166d74d 100644
--- a/templates/_history_trains.html.ep
+++ b/templates/_history_trains.html.ep
@@ -16,8 +16,11 @@
% }
<li class="collection-item">
<a href="<%= $detail_link %>">
- <span class="dep-line <%= $travel->{type} // q{} %>">
- <%= $travel->{type} %> <%= $travel->{line} // $travel->{no}%>
+ <span class="dep-line <%= ($travel->{type} // q{}) =~ tr{a-zA-Z_-}{}cdr %>">
+ % if (length($travel->{type}) < 5 and not $travel->{is_motis}) {
+ <%= $travel->{type} %>
+ % }
+ <%= $travel->{line} // $travel->{no}%>
</span>
</a>
@@ -34,8 +37,8 @@
<i class="material-icons">timer_off</i>
% } else {
%= $travel->{rt_arrival}->strftime('%H:%M');
- % if ($travel->{sched_arrival} != $travel->{rt_arrival}) {
- (<%= sprintf('%+d', ($travel->{rt_arrival}->epoch - $travel->{sched_arrival}->epoch) / 60) %>)
+ % if ($travel->{delay_arr} and int($travel->{delay_arr} / 60)) {
+ (<%= sprintf('%+d', $travel->{delay_arr} / 60) %>)
% }
% }
% }
@@ -52,8 +55,8 @@
% }
% else {
<%= $travel->{rt_departure}->strftime('%H:%M') %>
- % if ($travel->{sched_departure} != $travel->{rt_departure}) {
- (<%= sprintf('%+d', ($travel->{rt_departure}->epoch - $travel->{sched_departure}->epoch) / 60) %>)
+ % if ($travel->{delay_dep} and int($travel->{delay_dep} / 60)) {
+ (<%= sprintf('%+d', $travel->{delay_dep} / 60) %>)
% }
% }
<strong><%= $travel->{from_name} %></strong>
diff --git a/templates/_map.html.ep b/templates/_map.html.ep
index daa16f0..223bd68 100644
--- a/templates/_map.html.ep
+++ b/templates/_map.html.ep
@@ -1,16 +1,18 @@
-<div class="row">
- <div class="col s12">
- <div id="map" style="height: 70vh;">
+% if (stash('with_map_header') // 1) {
+ <div class="row">
+ <div class="col s12">
+ <div id="map" style="height: 70vh;">
+ </div>
</div>
</div>
-</div>
-<div class="row">
- <div class="col s12">
- <span style="color: #f03;">●</span> Ein-/Ausstiegsstation<br/>
- <span style="color: #673ab7;">—</span> Streckenverlauf oder Luftlinie
+ <div class="row">
+ <div class="col s12">
+ <span style="color: #f03;">●</span> Ein-/Ausstiegsstation<br/>
+ <span style="color: #673ab7;">—</span> Streckenverlauf oder Luftlinie
+ </div>
</div>
-</div>
+% }
<script>
var map = L.map('map').setView([51.306, 9.712], 6);
diff --git a/templates/_public_status_card.html.ep b/templates/_public_status_card.html.ep
index b463d15..32b193a 100644
--- a/templates/_public_status_card.html.ep
+++ b/templates/_public_status_card.html.ep
@@ -8,7 +8,7 @@
Unterwegs mit <%= include '_format_train', journey => $journey %>
% }
% elsif (stash('from_timeline')) {
- <a href="/p/<%= $name %>"><%= $name %></a>: <%= include '_format_train', journey => $journey %>
+ <a href="/status/<%= $name %>"><%= $name %></a>: <%= include '_format_train', journey => $journey %>
% }
% else {
<a href="/p/<%= $name %>"><%= $name %></a> ist unterwegs
@@ -19,9 +19,9 @@
% }
</span>
% if ($privacy->{comments_visible} and $journey->{comment}) {
- <p>„<%= $journey->{comment} %>“</p>
+ <div>„<%= $journey->{comment} %>“</div>
% }
- <p>
+ <div>
% if (not stash('from_profile') and not stash('from_timeline')) {
<div class="center-align">
%= include '_format_train', journey => $journey
@@ -60,8 +60,8 @@
<div class="progress" style="height: 1ex;">
<div class="determinate" style="width: <%= sprintf('%.2f', 100 * ($journey->{journey_completion} // 0)); %>%;"></div>
</div>
- </p>
- <p>
+ </div>
+ <div class="status-card-progress-annot">
<div style="float: left;">
<b><%= $journey->{dep_name} %></b><br/>
<b><%= $journey->{real_departure}->strftime('%H:%M') %></b>
@@ -92,20 +92,31 @@
% last;
% }
% if (($station->[2]{arr_countdown} // 0) > 0 and $station->[2]{arr}) {
- <%= $station->[0] %><br/><%= $station->[2]{arr}->strftime('%H:%M') %>
+ %= $station->[0]
+ <br/>
+ %= $station->[2]{arr}->strftime('%H:%M')
% if ($station->[2]{arr_delay}) {
- %= sprintf('(%+d)', $station->[2]{arr_delay} / 60);
+ %= sprintf('(%+d)', $station->[2]{arr_delay} / 60)
+ % }
+ % if ($station->[2]{load}{SECOND}) {
+ <br/>
+ %= include '_show_load_icons', station => $station
% }
% last;
% }
% if (($station->[2]{dep_countdown} // 0) > 0 and $station->[2]{dep}) {
- <%= $station->[0] %><br/>
+ %= $station->[0]
+ <br/>
% if ($station->[2]{arr}) {
<%= $station->[2]{arr}->strftime('%H:%M') %> →
% }
%= $station->[2]{dep}->strftime('%H:%M')
% if ($station->[2]{dep_delay}) {
- %= sprintf('(%+d)', $station->[2]{dep_delay} / 60);
+ %= sprintf('(%+d)', $station->[2]{dep_delay} / 60)
+ % }
+ % if ($station->[2]{load}{SECOND}) {
+ <br/>
+ %= include '_show_load_icons', station => $station
% }
% last;
% }
@@ -120,34 +131,46 @@
% }
% if (($station->[2]{arr_countdown} // 0) > 0 and $station->[2]{arr}) {
Nächster Halt:<br/>
- <%= $station->[0] %><br/><%= $station->[2]{arr}->strftime('%H:%M') %>
+ %= $station->[0]
+ <br/>
+ %= $station->[2]{arr}->strftime('%H:%M')
% if ($station->[2]{arr_delay}) {
- %= sprintf('(%+d)', $station->[2]{arr_delay} / 60);
+ %= sprintf('(%+d)', $station->[2]{arr_delay} / 60)
+ % }
+ % if ($station->[2]{load}{SECOND}) {
+ <br/>
+ %= include '_show_load_icons', station => $station
% }
% last;
% }
% if (($station->[2]{dep_countdown} // 0) > 0 and $station->[2]{arr} and $station->[2]{dep}) {
Aktueller Halt:<br/>
- <%= $station->[0] %><br/>
- <%= $station->[2]{arr}->strftime('%H:%M') %> →
- <%= $station->[2]{dep}->strftime('%H:%M') %>
+ %= $station->[0]
+ <br/>
+ %= $station->[2]{arr}->strftime('%H:%M')
+ →
+ %= $station->[2]{dep}->strftime('%H:%M')
% if ($station->[2]{dep_delay}) {
- %= sprintf('(%+d)', $station->[2]{dep_delay} / 60);
+ %= sprintf('(%+d)', $station->[2]{dep_delay} / 60)
+ % }
+ % if ($station->[2]{load}{SECOND}) {
+ <br/>
+ %= include '_show_load_icons', station => $station
% }
% last;
% }
% }
</div>
- </p>
+ </div>
% if ($journey->{extra_data}{cancelled_destination}) {
- <p style="margin-bottom: 2ex;">
+ <div style="margin-bottom: 2ex;">
<i class="material-icons tiny" aria-hidden="true">error</i>
Der Halt an der Zielstation <b><%=
$journey->{extra_data}{cancelled_destination} %></b> entfällt.
- </p>
+ </div>
% }
% if (@{$journey->{messages} // []} > 0 and $journey->{messages}[0]) {
- <p style="margin-bottom: 2ex;">
+ <div style="margin-top: 2ex;">
<ul>
% for my $message (reverse @{$journey->{messages} // []}) {
% if ($journey->{sched_departure}->epoch - $message->[0]->epoch < 1800) {
@@ -158,22 +181,95 @@
<li> <i class="material-icons tiny">info</i> <%= $message->[0]->strftime('%H:%M') %>: <%= $message->[1] %></li>
% }
</ul>
- </p>
+ </div>
% }
- </div>
- % if (not stash('from_timeline')) {
- <div class="card-action">
- % if ($journey->{traewelling_url}) {
- <a style="margin-right: 0;" href="<%= $journey->{traewelling_url} %>"><i class="material-icons left">timeline</i> Träwelling</a>
- % } else {
- % my $url = 'https://bahn.expert/details/' . $journey->{train_type} . ' ' . $journey->{train_no} . '/' . DateTime->now(time_zone => 'Europe/Berlin')->epoch . '000';
- <a style="margin-right: 0;" href="<%= $url %>"><i class="material-icons left">timeline</i> Zuglauf</a>
+ % if (@{$journey->{extra_data}{him_msg} // []}) {
+ <div style="margin-top: 2ex;">
+ <ul>
+ % for my $message (@{$journey->{extra_data}{him_msg} // []}) {
+ % if (not stash('from_timeline') or $message->{prio} and $message->{prio} eq 'HOCH') {
+ <li> <i class="material-icons tiny"><%= ($message->{prio} and $message->{prio} eq 'HOCH') ? 'warning' : 'info' %></i> <%= $message->{header} %> <%= $message->{lead} %></li>
+ % }
+ % }
+ </ul>
+ </div>
+ % }
+ % if (stash('station_coordinates')) {
+ <div id="map" style="height: 70vh;">
+ </div>
+ %= include '_map', with_map_header => 0, station_coordinates => stash('station_coordinates'), polyline_groups => stash('polyline_groups')
+ % }
+ % if ( @{$journey->{wagongroups} // []} ) {
+ % if (stash('from_timeline')) {
+ <div class="wagons" style="margin-top: 2ex;">
+ % for my $wagongroup (@{$journey->{wagongroups}}) {
+ %= $wagongroup->{desc} // $wagongroup->{name}
+ % if ($wagongroup->{designation}) {
+ „<%= $wagongroup->{designation} %>“
+ % }
+ % if ($wagongroup->{to}) {
+ → <%= $wagongroup->{to} %>
+ % }
+ <br/>
+ % }
+ </div>
% }
- % if ($journey->{extra_data}{trip_id}) {
- <a class="right" style="margin-right: 0;" href="https://dbf.finalrewind.org/map/<%= $journey->{extra_data}{trip_id} %>/<%= $journey->{train_line} || 0 %>?from=<%= $journey->{dep_name} %>&amp;to=<%= $journey->{arr_name} // '' %>"><i class="material-icons left">map</i> Karte</a>
+ % else {
+ <div class="wagons" style="margin-top: 2ex;">
+ Wagen:<br/>
+ %= include '_wagons', wagongroups => $journey->{wagongroups};
+ </div>
% }
- </div>
- % }
+ % }
+ % if (not stash('from_timeline')) {
+ <div style="margin-top: 2ex;">
+ Route:<br/>
+ % my $before = 1;
+ % my $within = 0;
+ % my $at_startstop = 0;
+ % for my $station (@{$journey->{route}}) {
+ % if (($station->[1] and $station->[1] == $journey->{dep_eva}) or $station->[0] eq $journey->{dep_name}) {
+ % $within = 1; $at_startstop = 1;
+ % }
+ % elsif ($journey->{arr_eva} and (($station->[1] and $station->[1] == $journey->{arr_eva}) or $station->[0] eq $journey->{arr_name})) {
+ % $within = 0; $at_startstop = 1;
+ % }
+ % else {
+ % $at_startstop = 0;
+ % }
+ <span style="color: #808080;">
+ % if ($before and $station->[2]{sched_dep}) {
+ %= $station->[2]{sched_dep}->strftime('%H:%M')
+ % }
+ % elsif (not $before and $station->[2]{sched_arr}) {
+ %= $station->[2]{sched_arr}->strftime('%H:%M')
+ % }
+ </span>
+ % if ($at_startstop or $within) {
+ %= $station->[0]
+ % }
+ % else {
+ <span style="color: #808080;"><%= $station->[0] %></span>
+ % }
+ <span>
+ %= include '_show_load_icons', station => $station
+ </span>
+ <span style="color: #808080;">
+ % if ($before and $station->[2]{rt_dep} and $station->[2]{dep_delay}) {
+ %= sprintf('%+d', $station->[2]{dep_delay} / 60)
+ % }
+ % elsif (not $before and $station->[2]{rt_arr} and $station->[2]{arr_delay}) {
+ %= sprintf('%+d', $station->[2]{arr_delay} / 60)
+ % }
+ </span>
+ % if (($station->[1] and $station->[1] == $journey->{dep_eva}) or $station->[0] eq $journey->{dep_name}) {
+ % $before = 0;
+ % }
+ <br/>
+ % }
+ </div>
+ % }
+ </div>
</div>
% }
% else {
@@ -186,7 +282,7 @@
% else {
<span class="card-title"><a href="/p/<%= $name %>"><%= $name %></a> ist gerade nicht eingecheckt</span>
% }
- <p>
+ <div>
% if ($journey->{arr_name}) {
Zuletzt gesehen
% if ($journey->{real_arrival}->epoch) {
@@ -198,7 +294,7 @@
in <b><%= $journey->{arr_name} %></b>
% }
% }
- </p>
+ </div>
</div>
</div>
% }
diff --git a/templates/_show_load_icons.html.ep b/templates/_show_load_icons.html.ep
new file mode 100644
index 0000000..21093b9
--- /dev/null
+++ b/templates/_show_load_icons.html.ep
@@ -0,0 +1,11 @@
+% if ($station->[2]{load}{SECOND}) {
+ % my ($first, $second) = load_icon($station->[2]{load});
+ % if ($first ne 'help_outline') {
+ <i class="material-icons tiny" aria-hidden="true"><%= $first %></i>
+ % }
+ <i class="material-icons tiny" aria-hidden="true"><%= $second %></i>
+% }
+% elsif ($station->[2]{efa_load}) {
+ % my ($icon) = efa_load_icon($station->[2]{efa_load});
+ <i class="material-icons tiny" aria-hidden="true"><%= $icon %></i>
+% }
diff --git a/templates/_timeline_link.html.ep b/templates/_timeline_link.html.ep
index 1a78279..4b9c2a5 100644
--- a/templates/_timeline_link.html.ep
+++ b/templates/_timeline_link.html.ep
@@ -1,5 +1,5 @@
<div>
- <a href="/timeline/in-transit">
+ <a class="timeline-link" href="/timeline/in-transit">
% if (@{$timeline} <= 2) {
<strong><%= $timeline->[0]->{followee_name} %></strong>
% }
diff --git a/templates/_wagons.html.ep b/templates/_wagons.html.ep
index 106709e..4090f11 100644
--- a/templates/_wagons.html.ep
+++ b/templates/_wagons.html.ep
@@ -1,13 +1,22 @@
% for my $wagongroup (@{$wagongroups // []}) {
- <%= $wagongroup->{name} %>
+ %= $wagongroup->{desc} // $wagongroup->{name}
% my ($wagon_number) = ($wagongroup->{name} =~ m{ ^ ICE 0* (\d+) $ }x);
- % if ($wagon_number and my $group_name = app->ice_name->{$wagon_number}) {
+ % if ($wagongroup->{designation}) {
+ „<%= $wagongroup->{designation} %>“
+ % }
+ % elsif ($wagon_number and my $group_name = app->ice_name->{$wagon_number}) {
„<%= $group_name %>“
% }
- als <b><%= $journey->{type} %> <%= $wagongroup->{no} %></b>
- von <b><%= $wagongroup->{from} %></b> nach <b><%= $wagongroup->{to} %></b><br/>
+ als <b><%= $wagongroup->{type} // $journey->{type} %> <%= $wagongroup->{no} %></b>
+ % if ($wagongroup->{from}) {
+ von <b><%= $wagongroup->{from} %></b>
+ % }
+ % if ($wagongroup->{to}) {
+ nach <b><%= $wagongroup->{to} %></b>
+ % }
+ <br/>
% for my $wagon (@{$wagongroup->{wagons}}) {
- % if (length($wagon->{id}) == 12) {
+ % if (length($wagon->{id}) == 12 or length($wagon->{id}) == 14) {
<span><%= substr($wagon->{id}, 0, 2) %></span><span><%= substr($wagon->{id}, 2, 2) %></span><span><%= substr($wagon->{id}, 4, 1) %></span><span class="wagonclass"><%= substr($wagon->{id}, 5, 3) %></span><span class="wagonnum"><%= substr($wagon->{id}, 8, 3) %></span><span class="checksum"><%= substr($wagon->{id}, 11) %></span>
% }
% elsif ($wagon->{id}) {
diff --git a/templates/about.html.ep b/templates/about.html.ep
index ea86bdf..e2b148d 100644
--- a/templates/about.html.ep
+++ b/templates/about.html.ep
@@ -1,14 +1,21 @@
<div class="row">
<div class="col s12">
<a href="https://finalrewind.org/projects/travelynx">travelynx</a> v<%= stash('version') // '???' %><br/>
- Entwickelt von <a href="https://finalrewind.org">derf</a><br/>
- <a href="<%= app->config->{ref}{source} // 'https://github.com/derf/travelynx' %>">Quelltext</a> lizensiert unter AGPL v3<br/><br/>
+ Entwickelt von <a href="https://finalrewind.org">derf</a>
+ und <a href="https://github.com/derf/travelynx/graphs/contributors">weiteren</a><br/>
+ <a href="<%= app->config->{ref}{source} // 'https://git.finalrewind.org/travelynx' %>">Quelltext</a> lizensiert unter AGPL v3<br/><br/>
Backends:
+ <a href="https://finalrewind.org/projects/Travel-Status-DE-DBRIS/">Travel::Status::DE::DBRIS</a>
+ v<%= $Travel::Status::DE::DBRIS::VERSION %>,
+ <a href="https://finalrewind.org/projects/Travel-Status-DE-EFA/">Travel::Status::DE::EFA</a>
+ v<%= $Travel::Status::DE::EFA::VERSION %>,
+ <a href="https://finalrewind.org/projects/Travel-Status-DE-HAFAS/">Travel::Status::DE::HAFAS</a>
+ v<%= $Travel::Status::DE::HAFAS::VERSION %>,
<a href="https://finalrewind.org/projects/Travel-Status-DE-IRIS/">Travel::Status::DE::IRIS</a>
v<%= $Travel::Status::DE::IRIS::VERSION %> und
- <a href="https://finalrewind.org/projects/Travel-Status-DE-DeutscheBahn/">Travel::Status::DE::HAFAS</a>
- v<%= $Travel::Status::DE::HAFAS::VERSION %><br/>
- <a href="http://data.deutschebahn.com/dataset/data-haltestellen">Haltestellendaten</a>
+ <a href="https://finalrewind.org/projects/Travel-Status-MOTIS/">Travel::Status::MOTIS</a>
+ v<%= $Travel::Status::MOTIS::VERSION %><br/>
+ Haltestellendaten
© DB Station&amp;Service AG,
Europaplatz 1,
10557 Berlin, lizensiert unter CC-BY 4.0
@@ -16,6 +23,19 @@
</div>
<div class="row">
+ <div class="col s12">
+ <p>
+ Travelynx ist ein kostenfreies, privat betriebenes Projekt ohne
+ Verfügbarkeitsgarantie. Unangekündigte Downtimes oder eine
+ kurzfristige Einstellung dieser Seite sind nicht vorgesehen, aber
+ möglich. Feature Requests, Bug Reports und sonstige Nachrichten
+ werden je nach Kapazität und Motivation zeitnah, verzögert oder gar
+ nicht bearbeitet / beantwortet.
+ </p>
+ </div>
+</div>
+
+<div class="row">
<div class="col s12 m12 l4 center-align" style="margin-top: 1em;">
<a href="https://social.skyshaper.org/derf" class="waves-effect waves-light btn"><i class="material-icons left">message</i>Kontakt</a>
</div>
diff --git a/templates/account.html.ep b/templates/account.html.ep
index 7f689c2..e4bf38d 100644
--- a/templates/account.html.ep
+++ b/templates/account.html.ep
@@ -28,9 +28,6 @@
% elsif ($success eq 'use_history') {
<span class="card-title">Einstellungen zu vorgeschlagenen Verbindungen geändert</span>
% }
- % elsif ($success eq 'external') {
- <span class="card-title">Einstellungen zu externen Diensten geändert</span>
- % }
% elsif ($success eq 'webhook') {
<span class="card-title">Web Hook aktualisiert</span>
% }
@@ -126,6 +123,8 @@
<tr>
<th scope="row">Träwelling</th>
<td>
+ Wird wegen Inkompatibilität zwischen bahn.de und transitous derzeit nicht unterstützt
+ <!--
<a href="/account/traewelling"><i class="material-icons">edit</i></a>
% if (not ($traewelling->{token})) {
<span style="color: #999999;">Nicht verknüpft</span>
@@ -148,22 +147,11 @@
– Checkins in travelynx werden zu Träwelling weitergereicht
% }
% }
+ -->
</td>
</tr>
% }
<tr>
- <th scope="row">Externe Dienste</th>
- <td>
- <a href="/account/services"><i class="material-icons">edit</i></a>
- % if ($acc->{sb_name}) {
- Abfahrtstafel: <%= $acc->{sb_name} %>
- % }
- % else {
- <span style="color: #999999;">Keine</span>
- % }
- </td>
- </tr>
- <tr>
<th scope="row">Registriert am</th>
<td><%= $acc->{registered_at}->strftime('%d.%m.%Y %H:%M') %></td>
</tr>
diff --git a/templates/add_intransit.html.ep b/templates/add_intransit.html.ep
new file mode 100644
index 0000000..9d711c9
--- /dev/null
+++ b/templates/add_intransit.html.ep
@@ -0,0 +1,93 @@
+<h1>Manuell einchecken</h1>
+% if ($error) {
+ <div class="row">
+ <div class="col s12">
+ <div class="card caution-color">
+ <div class="card-content white-text">
+ <span class="card-title">Ungültige Eingabe</span>
+ <p><%= $error %></p>
+ </div>
+ </div>
+ </div>
+ </div>
+% }
+<div class="row">
+ <div class="col s12">
+ <p>
+ Falls die gesuchte Abfahrt nicht vom ausgewählten Backend verfügbar ist, z.B. da es sich um eine Sonderfahrt handelt, ist hier ein manueller Checkin möglich.
+ Nach dem Checkin werden alle Daten so beibehalten wie sie eingegeben wurden; Änderungen sind erst nach dem Auschecken möglich.
+ </p>
+ <ul>
+ <li>Eingabe der Fahrt als „Typ Linie Nummer“ oder „Typ Nummer“, z.B.
+ „ICE 100“, „S 1 31133“ oder „ABR RE11 26720“</li>
+ <li>Wenn Nummer nicht bekannt oder vorhanden: einen beliebigen Integer eintragen, z.B. „S 5X 0“ oder „U 11 0“</li>
+ <li>Zeitangaben im Format DD.MM.YYYY HH:MM</li>
+ <li>Das ausgewählte Backend bestimmt die verfügbaren Halte für Start, Ziel und Route. Siehe auch <a href="/static/stops.csv">stops.csv</a></li>
+ </ul>
+ </div>
+</div>
+<div class="row">
+ <div class="col s12 center-align">
+ % if (current_user->{backend_id}) {
+ <a href="/account/select_backend?redirect_to=/checkin/add" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= current_user->{backend_name} %></a>
+ % }
+ % else {
+ <a href="/account/select_backend?redirect_to=/checkin/add" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">train</i>IRIS</a>
+ % }
+ </div>
+</div>
+%= form_for '/checkin/add' => (method => 'POST') => begin
+ %= csrf_field
+ <div class="row">
+ <div class="input-field col s12">
+ %= text_field 'train', id => 'train', class => 'validate', required => undef, pattern => '[0-9a-zA-Z]+ +[0-9a-zA-Z]* *[0-9]+'
+ <label for="train">Fahrt (Typ Linie Nummer)</label>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ %= text_field 'dep_station', id => 'dep_station', class => 'autocomplete validate', autocomplete => 'off', required => undef
+ <label for="dep_station">Start (Name oder ID)</label>
+ </div>
+ <div class="input-field col s12">
+ %= text_field 'sched_departure', id => 'sched_departure', class => 'validate', required => undef, pattern => '[0-9][0-9]?[.][0-9][0-9]?[.][0-9][0-9][0-9][0-9] +[0-9][0-9]:[0-9][0-9]'
+ <label for="sched_departure">Geplante Abfahrt</label>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ %= text_field 'arr_station', id => 'arr_station', class => 'autocomplete validate', autocomplete => 'off', required => undef
+ <label for="arr_station">Ziel (Name oder ID)</label>
+ </div>
+ <div class="input-field col s12">
+ %= text_field 'sched_arrival', id => 'sched_arrival', class => 'validate', required => undef, pattern => '[0-9][0-9]?[.][0-9][0-9]?[.][0-9][0-9][0-9][0-9] +[0-9][0-9]:[0-9][0-9]'
+ <label for="sched_arrival">Geplante Ankunft</label>
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ %= text_area 'route', id => 'route', class => 'materialize-textarea'
+ <label for="route">Halte (optional)</label><br/>
+ Eine Station pro Zeile, wahlweise Unterwegshalte oder komplette Route<br/>
+ Format: <i>Name</i> oder <i>Name</i> @ <i>Zeitpunkt</i> (inkl. Datum, siehe oben)
+ </div>
+ </div>
+ <div class="row">
+ <div class="input-field col s12">
+ %= text_field 'comment'
+ <label for="comment">Kommentar</label>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col s3 m3 l3">
+ </div>
+ <div class="col s6 m6 l6 center-align">
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="save">
+ Einchecken
+ <i class="material-icons right">send</i>
+ </button>
+ </div>
+ <div class="col s3 m3 l3">
+ </div>
+ </div>
+%= end
diff --git a/templates/add_journey.html.ep b/templates/add_journey.html.ep
index c543781..cade37e 100644
--- a/templates/add_journey.html.ep
+++ b/templates/add_journey.html.ep
@@ -34,9 +34,21 @@
„ICE 100“, „S 1 31133“ oder „ABR RE11 26720“</li>
<li>Wenn Nummer nicht bekannt oder vorhanden: einen beliebigen Integer eintragen, z.B. „S 5X 0“ oder „U 11 0“</li>
<li>Zeitangaben im Format DD.MM.YYYY HH:MM</li>
+ <li>Das ausgewählte Backend bestimmt die verfügbaren Halte für Start, Ziel und Route. Siehe auch <a href="/static/stops.csv">stops.csv</a></li>
</ul>
</div>
</div>
+<div class="row">
+ <div class="col s12 center-align">
+ % my $self_link = url_for('add_journey');
+ % if (current_user->{backend_id}) {
+ <a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= current_user->{backend_name} %></a>
+ % }
+ % else {
+ <a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">train</i>IRIS</a>
+ % }
+ </div>
+</div>
%= form_for '/journey/add' => (method => 'POST') => begin
%= csrf_field
<div class="row">
@@ -54,7 +66,7 @@
<div class="row">
<div class="input-field col s12">
%= text_field 'dep_station', id => 'dep_station', class => 'autocomplete validate', autocomplete => 'off', required => undef
- <label for="dep_station">Start (Name oder DS100)</label>
+ <label for="dep_station">Start (Name oder ID)</label>
</div>
<div class="input-field col s12">
%= text_field 'sched_departure', id => 'sched_departure', class => 'validate', required => undef, pattern => '[0-9][0-9]?[.][0-9][0-9]?[.][0-9][0-9][0-9][0-9] +[0-9][0-9]:[0-9][0-9]'
@@ -68,7 +80,7 @@
<div class="row">
<div class="input-field col s12">
%= text_field 'arr_station', id => 'arr_station', class => 'autocomplete validate', autocomplete => 'off', required => undef
- <label for="arr_station">Ziel (Name oder DS100)</label>
+ <label for="arr_station">Ziel (Name oder ID)</label>
</div>
<div class="input-field col s12">
%= text_field 'sched_arrival', id => 'sched_arrival', class => 'validate', required => undef, pattern => '[0-9][0-9]?[.][0-9][0-9]?[.][0-9][0-9][0-9][0-9] +[0-9][0-9]:[0-9][0-9]'
@@ -82,7 +94,9 @@
<div class="row">
<div class="input-field col s12">
%= text_area 'route', id => 'route', class => 'materialize-textarea'
- <label for="route">Unterwegshalte (optional, eine Station pro Zeile, DS100 möglich)</label>
+ <label for="route">Halte (optional)</label><br/>
+ Eine Station pro Zeile, wahlweise Unterwegshalte oder komplette Route<br/>
+ Format: <i>Name</i> oder <i>Name</i> @ <i>Zeitpunkt</i> (inkl. Datum, siehe oben)
</div>
</div>
<div class="row">
diff --git a/templates/api_documentation.html.ep b/templates/api_documentation.html.ep
index 9c9ee1f..099474c 100644
--- a/templates/api_documentation.html.ep
+++ b/templates/api_documentation.html.ep
@@ -30,12 +30,18 @@
"actionTime" : 1234567, (UNIX-Timestamp des letzten Checkin/Checkout)<br/>
"checkedIn" : true / false,<br/>
"comment": "Kommentar",<br/>
+ "backend": {<br/>
+ "id": 1,<br/>
+ "name": "DB",<br/>
+ "type": "HAFAS",<br/>
+ },<br/>
"fromStation" : { (letzter Checkin)<br/>
"name" : "Essen Hbf",<br/>
"ds100" : "EE", (ggf. null)<br/>
"uic" : 8000098,<br/>
"latitude" : 51.451355,<br/>
"longitude" : 7.014793,<br/>
+ "platform" : "12", (ggf. null)<br/>
"scheduledTime": 1556083680,<br/>
"realTime": 1556083680<br/>
},<br/>
@@ -45,6 +51,7 @@
"uic" : 8001896,<br/>
"latitude" : 51.422853,<br/>
"longitude" : 7.023296,<br/>
+ "platform" : "2", (ggf. null)<br/>
"scheduledTime": 1556083980, (ggf. null)<br/>
"realTime": 1556083980 (ggf. null)<br/>
},<br/>
@@ -122,11 +129,13 @@
{<br/>
"token" : "<%= $uid %>-<%= $token->{travel} // 'TOKEN' %>",<br/>
"action" : "checkin",<br/>
+ "dbris" : "bahn.de", (DBRIS-Instanz – Default: bahn.de)<br/>
+ "hafas" : null, (HAFAS-Instanz, falls verwendet, sonste null)<br/>
"train" : {<br/>
- "journeyID" : "1|1426396|4|80|19082023",<br/>
+ "journeyID" : "2|#VN#1#ST#1742845592#PI#0#ZI#315136#TA#0#DA#270325#1S#8000080#1T#1841#LS#8006486#LT#2024#PU#80#RT#1#CA#RE#ZE#10773#ZB#RE10773#PC#3#FR#8000080#FT#1841#TO#8006486#TT#2024#",<br/>
}<br/>
- "fromStation" : 651806, (Name oder EVA-Nummer)<br/>
- "toStation" : 654645, (optional, Name oder EVA-Nummer)<br/>
+ "fromStation" : 8000080, (Name oder EVA-Nummer – bei bahn.de nur EVA-Nummer)<br/>
+ "toStation" : 8006486, (optional, Name oder EVA-Nummer – bei bahn.de nur EVA-Nummer)<br/>
"comment" : "Beliebiger Text" (optional, überschreibt vorherigen Kommentar)<br/>
}
</p>
diff --git a/templates/bad_gateway.html.ep b/templates/bad_gateway.html.ep
new file mode 100644
index 0000000..07bf29e
--- /dev/null
+++ b/templates/bad_gateway.html.ep
@@ -0,0 +1,27 @@
+<div class="row">
+ <div class="col s12">
+ <div class="card caution-color">
+ <div class="card-content white-text">
+ <span class="card-title">502 Bad Gateway</span>
+ <p>
+ Das von travelynx genutzte Backend hat einen Fehler zurückgegeben.
+ travelynx hat keine Möglichkeiten, diese Situation zu beheben.
+ % if (stash('select_new_backend')) {
+ Versuche es in ein paar Sekunden bis Minuten noch einmal oder <a href="/account/select_backend">wähle ein anderes Backend</a>.
+ % }
+ % else {
+ Versuche es in ein paar Sekunden bis Minuten noch einmal.
+ % }
+ </p>
+ </div>
+ </div>
+ </div>
+</div>
+<div class="row">
+ <div class="col s12">
+ <p>Details:</p>
+ <p style="font-family: monospace;">
+ %= $message
+ </p>
+ </div>
+</div>
diff --git a/templates/changelog.html.ep b/templates/changelog.html.ep
index 09126a8..0d1ecc5 100644
--- a/templates/changelog.html.ep
+++ b/templates/changelog.html.ep
@@ -2,6 +2,259 @@
<div class="row">
<div class="col s12 m1 l1">
+ 2.15
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Manuelle Checkins. Diese verhalten sich analog zu manuell
+ eingetragenen Fahrten, werden jedoch bis zur planmäßigen
+ Ankunftszeit als Checkin behandelt. Manuelle Echtzeitdaten-Updates
+ werden nicht unterstützt. Manuelle Checkins sind nur an Halten
+ möglich, die dem ausgewählten Backend bekannt sind. Ggf. wird
+ dieses Feature später um eine Möglichkeit für Echtzeitdaten-Updates
+ und/oder eine API erweitert.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Erfassung des Betreibers einer Fahrt, sofern verfügbar.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Verbesserung">star</i>
+ EFA-Backends werden nun fast vollständig unterstützt und sind nicht
+ mehr experimentell.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Das manuelle Eintragen von Fahrten ist nun wieder möglich. Zudem
+ kann dabei nun ein beliebiges Backend ausgewählt werden; das
+ ausgewählte Backend bestimmt die verfügbaren Halte.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.14
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Experimentelle Unterstützung für Checkins via EFA-Backends.
+ Teilweise ist ein Checkin nur bei Fahrten mit Echtzeitdaten
+ möglich. Hierbei handelt es sich nach aktuellem Stand um eine
+ Einschränkung der verwendeten Backends. Unterstützung für
+ ausfallende Fahrten folgt später.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.13
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Experimentelle Unterstützung für Checkins via MOTIS-Backends
+ (derzeit transitous und RNV). Vielen Dank an <a href="https://github.com/networkException">networkException</a>
+ für die Implementierung der API und Einbindung in travelynx.
+ Träwelling-Synchronisierung ist noch nicht wiederhergestellt.
+ Time zones are currently somewhat wibbly-wobbly timey-wimey.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.12
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Kartografische Visualisierung der Route bei eigenen Checkins und auf
+ der Statusseite sowie Angaben zu Meldungen, Rollmaterial, Route und
+ Auslastung auf der Statusseite. Feinheiten wie die Markierung der
+ geschätzten aktuellen Zugposition oder eine regelmäßige
+ Aktualisierung ohne Zurücksetzen der Kartenansicht folgen später.
+ Die Kartenlinks zu dbf.finalrewind.org entfallen.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Ankündigung">announcement</i>
+ Das IRIS-TTS-Backend der Deutschen Bahn wird wegen zunehmend
+ schlechter Datenanreicherunngsmöglichkeiten nicht mehr
+ weiterentwickelt. Bei Checkins per IRIS-TTS stehen regelmäßig keine
+ Echtzeitdaten und insbesondere bei Nebenbahnen auch keine
+ Kartendaten zur Verfügung. In diesem Fall fehlt auch die
+ ersatzwiese Visualisierung der Luftlinie zwischen den
+ Unterwegshalten. Dies betrifft auch die Visualisierung in der
+ Fahrtenkarte.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Ankündigung">announcement</i>
+ Derzeit besteht wegen inkompatibler Backends keine
+ Synchronisierungsmöglichkeit zwischen Träwelling (transitous MOTIS)
+ und travelynx (DB IRIS-TTS / DB HAFAS / bahn.de).
+ MOTIS-Unterstützung in travelynx ist in Arbeit.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.11
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Neues Backend: bahn.de. Somit steht nach Abschaltung von DB HAFAS
+ und VRN HAFAS wieder ein Backend zur Verfügung, welches für
+ innerdeutschen Nah-, Regional- und Fernverkehr geeignet ist und
+ eine Synchronisierung mit Träwelling unterstützt. Teile der
+ Implementierung können noch unvollständig sein. Ebenso besteht die
+ Möglichkeit, dass es wegen Rate Limits auf Seiten von bahn.de nicht
+ immer zuverlässig nutzbar ist.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.10
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Neue HAFAS-Backends: PKP, SaarVV.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bug">warning</i> Das DB
+ HAFAS-Backend wurde am 8. Januar 2025 abgeschaltet und wird von
+ travelynx daher seit v2.9.11 nicht mehr angeboten. Als vorläufiger
+ Ersatz bietet sich das VRN HAFAS-Backend an. Eine Wieder-Anbindung
+ der DB mittels Travel::Status::DE::DBRIS ist in Arbeit. Bis dahin
+ ist keine Synchronisierung mit Traewelling möglich.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Administration">announcement</i>
+ Das PKP HAFAS befindet sich hinter einem GeoIP-Filter und wird
+ daher in travelynx-Installationen außerhalb von travelynx.de
+ standardmäßig nicht angeboten. Sofern die travelynx-Instanz auf
+ einer geeigneten IP-Adresse betrieben wird oder eine solche per
+ Proxy erreichbar ist, lässt es sich über einen Eintrag in
+ travelynx.conf aktivieren. Als Nebenwirkung davon kann auch auf
+ beliebige andere HAFAS-Instanzen bei Bedarf über einen
+ Instanz-spezifischen Proxy zugegriffen werden.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.9
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Neue HAFAS-Backends: BVG, KVB, mobiliteit, RMV, RSAG, STV, VMT,
+ VOS, VRN, ZVV.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ HAFAS-Backends: verbesserte Unterstützung für Ringlinien.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Verbesserte Unterstützung für uneindeutige Stationsnamen. Berlin
+ Hbf ist beispielsweise intern in „Berlin Hbf“ (Gleise 1 bis 8),
+ „Berlin Hbf“ (Gleise 11 bis 14) und „Berlin Hbf (S-Bahn)“ (Gleise
+ 15 und 16) getrennt. Teile von travelynx gingen in der
+ Vergangenheit fälschlich davon aus, dass es keine Stationen mit
+ identischen Namen, aber unterschiedlichen internen IDs gebe.
+ Dies hat u.a. bei Fahrten von/nach Berlin Hbf und innerhalb von
+ Karlsruhe zu interessanten Bugs geführt.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bug">warning</i>
+ Reisen, die in travelynx 2.8.0 bis 2.8.30 mittels IRIS-Backend
+ geloggt wurden, können in Einzelfällen fehlerhafte Stationsangaben
+ enthalten. Der Bug betrifft alle Fahrten von/zu Stationen, die in
+ der von travelynx genutzten Stationsdatenbank zum Checkin-Zeitpunkt
+ nicht bekannt waren. Eine nachträgliche Korrektur dieser Fahrten
+ folgt ggf. in einem späteren Release.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Administration">announcement</i>
+ travelynx verlinkt bei Registrierung und Anmeldung nun
+ instanzspezifische <a href="/tos">Nutzungsbedingungen</a>. Admins
+ sollten beim Update auf diese Version
+ templates/terms-of-service.html.ep anlegen. Die Nutzungsbedingungen
+ können beispielsweise Richtlinien für die Freitexte in
+ Checkin-Kommentaren und auf der Profilseite vorgeben oder
+ allgemeine Hinweise und Bedingungen zur Verfügbarkeit der
+ jeweiligen Instanz beinhalten.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.8
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Neues Feature">add</i>
+ Unterstützung von HAFAS-Backends abseits der Deutschen Bahn. Somit
+ sind zumeist akkurate Echtzeit- und Routendaten für Checkins u.a.
+ in Aachen, Berlin/Brandenburg, Hessen, Sachsen-Anhalt,
+ Schleswig-Holstein, Österreich und der Schweiz verfügbar.
+ Das Backend muss vor dem Checkin explizit ausgewählt werden.
+ Eine Synchronisierung mit Traewelling wird nur für DB (IRIS-TTS) –
+ vormals „Schienenverkehr“ – und DB (HAFAS) – vormals „Nahverkehr“ –
+ durchgeführt. Manuell eingetragene Fahrten sind vorerst ebenfalls
+ auf DB (HAFAS) beschränkt.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Ankündigung">announcement</i>
+ Stationssuche und Verbindungsvorschläge berücksichtigen nur noch
+ das ausgewählte Backend. Die bisherige Verknüpfung von DB (IRIS-TTS)
+ und DB (HAFAS) entfällt.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
+ 2.7
+ </div>
+ <div class="col s12 m11 l11">
+ <p>
+ <i class="material-icons left" aria-label="Verbesserung">star</i>
+ Checkins via Nahverkehr (HAFAS) speichern nun Polylines (Routen für
+ die Fahrtenkarte) und Wagenreihungen, sofern verfügbar. Sie sind
+ damit fast identisch zu Checkins via Schienenverkehr (IRIS); es
+ fehlen im Wesentlichen lediglich die mit Zeitstempel versehenen
+ Verspätungs- und Störungsmeldungen.
+ <p/>
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Verbesserte (aber weiterhin nicht perfekte) Unterstützung für
+ Ringlinien.
+ </p>
+ <p>
+ <i class="material-icons left" aria-label="Bugfix">build</i>
+ Korrekte Verlinkung von HAFAS-basierten Abfahrtstafeln bei den
+ Unterwegshalten des aktuellen Checkins im Nahverkehrsmodus. Die
+ Konfigurationsmöglichkeit zur Auswahl zwischen bahn.expert und DBF
+ unter Account → Externe Dienste besteht wegen der Abhängigkeit des
+ Diensts vom genutzten Backend und zwecks besserer Wartbarkeit von
+ travelynx nun nicht mehr.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12 m1 l1">
2.6
</div>
<div class="col s12 m11 l11">
diff --git a/templates/departures.html.ep b/templates/departures.html.ep
index 6aac482..6df48a8 100644
--- a/templates/departures.html.ep
+++ b/templates/departures.html.ep
@@ -1,26 +1,33 @@
<div class="row">
- <div class="col s12">
- <h2>
- <i class="material-icons " aria-hidden="true"><%= param('hafas') ? 'directions' : 'train' %></i>
+ <div class="col s8">
+ <strong style="font-size: 120%;">
<%= $station %>
- </h2>
+ </strong>
% for my $related_station (sort { $a->{name} cmp $b->{name} } @{$related_stations}) {
+ <%= $related_station->{name} %> <br/>
% }
</div>
-</div>
-% if ($api_link) {
-<div class="row">
- <div class="col s12 center-align">
- % if (param('hafas')) {
- <a href="<%= $api_link %>" class="btn-small"><i class="material-icons left" aria-hidden="true">train</i>zum Schienenverkehr</a>
- % }
- % else {
- <a href="<%= $api_link %>" class="btn-small"><i class="material-icons left" aria-hidden="true">directions</i>zum Nahverkehr</a>
- % }
+ <div class="col s4 center-align">
+ % my $self_link = url_for('sstation', station => $station // param('station'));
+ % if (param('dbris')) {
+ <a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= param('dbris') %></a>
+ % }
+ % elsif (param('hafas')) {
+ <a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= param('hafas') %></a>
+ % }
+ % elsif (param('motis')) {
+ <a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= param('motis') %></a>
+ % }
+ % else {
+ % if ($user->{backend_id}) {
+ <a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">directions</i><%= $user->{backend_name} %></a>
+ % }
+ % else {
+ <a href="/account/select_backend?redirect_to=<%= $self_link %>" class="btn-small btn-flat"><i class="material-icons left" aria-hidden="true">train</i>IRIS</a>
+ % }
+ % }
</div>
</div>
-% }
% my $have_connections = 0;
% if ($user_status->{checked_in}) {
@@ -29,7 +36,13 @@
<div class="card">
<div class="card-content">
<span class="card-title">Aktuell eingecheckt</span>
- <p>In <%= $user_status->{train_type} %> <%= $user_status->{train_no} %>
+ <p>In
+ % if ( not $user_status->{is_motis} ) {
+ <%= $user_status->{train_type} %>
+ % }
+
+ <%= $user_status->{train_line} // $user_status->{train_no} %>
+
% if ( $user_status->{arr_name}) {
von <%= $user_status->{dep_name} %> nach <%= $user_status->{arr_name} %>
% }
@@ -40,10 +53,10 @@
</div>
<div class="card-action">
% if ($can_check_out) {
- <a class="action-undo" data-id="in_transit" data-checkints="<%= $user_status->{timestamp}->epoch %>" style="margin-right: 0;">
+ <a class="action-undo" data-hafas="<%= param('hafas') // q{} %>" data-id="in_transit" data-checkints="<%= $user_status->{timestamp}->epoch %>" style="margin-right: 0;">
<i class="material-icons left" aria-hidden="true">undo</i> Rückgängig
</a>
- <a class="action-checkout right" data-station="<%= $eva %>" data-force="1">
+ <a class="action-checkout right" data-hafas="<%= param('hafas') // q{} %>" data-station="<%= $eva %>" data-force="1">
Hier auschecken
</a>
% }
@@ -51,7 +64,7 @@
<a class="action-undo" data-id="in_transit" data-checkints="<%= $user_status->{timestamp}->epoch %>" style="margin-right: 0;">
<i class="material-icons left" aria-hidden="true">undo</i> Rückgängig
</a>
- <a class="action-checkout right" data-station="<%= $eva %>" data-force="1">
+ <a class="action-checkout right" data-hafas="<%= param('hafas') // q{} %>" data-station="<%= $eva %>" data-force="1">
<i class="material-icons left" aria-hidden="true">gps_off</i>
Hier auschecken
</a>
@@ -92,8 +105,8 @@
<div class="row">
<div class="col s4 center-align">
- % if ($hafas) {
- <a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({hafas => 1, timestamp => $datetime->clone->subtract(hours => 1)->epoch}) %>"><i class="material-icons left" aria-hidden="true">chevron_left</i><span class="hide-on-small-only">früher</span></a>
+ % if ($dbris or $efa or $hafas or $motis) {
+ <a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({dbris => $dbris, hafas => $hafas, timestamp => $datetime->clone->subtract(hours => 1)->epoch}) %>"><i class="material-icons left" aria-hidden="true">chevron_left</i><span class="hide-on-small-only">früher</span></a>
% }
</div>
<div class="col s4 center-align">
@@ -102,8 +115,8 @@
% }
</div>
<div class="col s4 center-align">
- % if ($hafas) {
- <a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({hafas => 1, timestamp => $datetime->clone->add(hours => 1)->epoch}) %>"><span class="hide-on-small-only">später</span><i class="material-icons right" aria-hidden="true">chevron_right</i></a>
+ % if ($dbris or $efa or $hafas or $motis) {
+ <a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({dbris => $dbris, hafas => $hafas, timestamp => $datetime->clone->add(hours => 1)->epoch}) %>"><span class="hide-on-small-only">später</span><i class="material-icons right" aria-hidden="true">chevron_right</i></a>
% }
</div>
</div>
@@ -133,13 +146,28 @@
Fahrt auswählen zum Einchecken.
% }
% else {
- Keine Abfahrten gefunden. Ein Checkin ist frühestens 30 Minuten vor
- und maximal 120 Minuten nach Abfahrt möglich.
+ % if ($dbris or $hafas) {
+ Keine Abfahrten im ausgewählten Zeitfenster
+ (<%= $datetime->strftime('%d.%m.%Y %H:%M') %> ± 30min).
+ % }
+ % else {
+ Keine Abfahrten gefunden. Ein Checkin ist frühestens 30 Minuten vor
+ und maximal 120 Minuten nach Abfahrt möglich.
+ % }
% }
</p>
% if (not $user_status->{checked_in} or ($can_check_out and $user_status->{arr_eva} and $user_status->{arrival_countdown} <= 0)) {
- % if ($hafas) {
- %= include '_departures_hafas', results => $results;
+ % if ($dbris) {
+ %= include '_departures_dbris', results => $results, dbris => $dbris;
+ % }
+ % elsif ($efa) {
+ %= include '_departures_efa', results => $results, efa => $efa;
+ % }
+ % elsif ($hafas) {
+ %= include '_departures_hafas', results => $results, hafas => $hafas;
+ % }
+ % elsif ($motis) {
+ %= include '_departures_motis', results => $results, motis => $motis;
% }
% else {
%= include '_departures_iris', results => $results;
@@ -147,3 +175,26 @@
% }
</div>
</div>
+
+<div class="row">
+ <div class="col s4 center-align">
+ % if ($dbris or $efa or $hafas or $motis) {
+ <a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({dbris => $dbris, hafas => $hafas, timestamp => $datetime->clone->subtract(hours => 1)->epoch}) %>"><i class="material-icons left" aria-hidden="true">chevron_left</i><span class="hide-on-small-only">früher</span></a>
+ % }
+ </div>
+ <div class="col s4 center-align">
+ </div>
+ <div class="col s4 center-align">
+ % if ($dbris or $efa or $hafas or $motis) {
+ <a class="btn-small" href="<%= url_for('sstation', station => param('station'))->query({dbris => $dbris, hafas => $hafas, timestamp => $datetime->clone->add(hours => 1)->epoch}) %>"><span class="hide-on-small-only">später</span><i class="material-icons right" aria-hidden="true">chevron_right</i></a>
+ % }
+ </div>
+</div>
+
+% if (not $user_status->{checked_in}) {
+ <div class="row">
+ <div class="col s12 center-align">
+ <a class="btn-small" href="<%= url_for('checkinadd')->query({dbris => $dbris, efa => $efa, hafas => $hafas, motis => $motis, dep_station => $station}) %>"><i class="material-icons left" aria-hidden="true">add</i><span>manuell einchecken</span></a>
+ </div>
+ </div>
+% }
diff --git a/templates/disambiguation.html.ep b/templates/disambiguation.html.ep
index 270aa99..af7d1dd 100644
--- a/templates/disambiguation.html.ep
+++ b/templates/disambiguation.html.ep
@@ -13,7 +13,7 @@
<div class="col s12">
<ul class="suggestions">
% for my $suggestion (@{$suggestions // []}) {
- <li><a href="<%= url_for('station' => $suggestion->{eva}) . (param('hafas') ? '?hafas=1' : q{}) %>"><%= $suggestion->{name} %></a></li>
+ <li><a href="<%= url_for('station' => $suggestion->{eva}) . (param('hafas') ? '?hafas=' . param('hafas') : q{}) %>"><%= $suggestion->{name} %></a></li>
% }
</ul>
</div>
diff --git a/templates/exception.html.ep b/templates/exception.html.ep
index ec01ad2..9b8697c 100644
--- a/templates/exception.html.ep
+++ b/templates/exception.html.ep
@@ -20,8 +20,15 @@
Timestamp:
%= DateTime->now(time_zone => 'Europe/Berlin')->strftime("%d/%b/%Y:%H:%M:%S %z")
<br/><br/>
- Message:
- %= ref($exception) ? (split(qr{\n}, $exception->message))[0] : $exception
+ % if (ref($exception)) {
+ Trace:<br/>
+ % for my $line (split(qr{\n}, $exception->message)) {
+ <%= $line %><br/>
+ % }
+ % }
+ % else {
+ Message: <%= $exception %>
+ % }
</p>
</div>
</div>
diff --git a/templates/gateway_timeout.html.ep b/templates/gateway_timeout.html.ep
new file mode 100644
index 0000000..9cf8690
--- /dev/null
+++ b/templates/gateway_timeout.html.ep
@@ -0,0 +1,27 @@
+<div class="row">
+ <div class="col s12">
+ <div class="card caution-color">
+ <div class="card-content white-text">
+ <span class="card-title">504 Gateway Timeout</span>
+ <p>
+ Das von travelynx genutzte Backend hat nicht rechtzeitig reagiert.
+ travelynx hat keine Möglichkeiten, diese Situation zu beheben.
+ % if (stash('select_new_backend')) {
+ Versuche es in ein paar Sekunden bis Minuten noch einmal oder <a href="/account/select_backend">wähle ein anderes Backend</a>.
+ % }
+ % else {
+ Versuche es in ein paar Sekunden bis Minuten noch einmal.
+ % }
+ </p>
+ </div>
+ </div>
+ </div>
+</div>
+<div class="row">
+ <div class="col s12">
+ <p>Details:</p>
+ <p style="font-family: monospace;">
+ %= $message
+ </p>
+ </div>
+</div>
diff --git a/templates/history_by_month.html.ep b/templates/history_by_month.html.ep
index 9ad7031..c3b1004 100644
--- a/templates/history_by_month.html.ep
+++ b/templates/history_by_month.html.ep
@@ -4,6 +4,12 @@
%= include '_history_stats', stats => stash('statistics');
% }
+<div class="row">
+ <div class="col s12 m12 l12 center-align">
+ <a href="/history/map?filter_from=<%= $filter_from->strftime('%d.%m.%Y') %>&amp;filter_to=<%= $filter_to->strftime('%d.%m.%Y') %>" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">map</i> Karte</a>
+ </div>
+</div>
+
% if (stash('journeys')) {
%= include '_history_trains', date_format => '%d.%m.', journeys => stash('journeys');
% }
diff --git a/templates/history_map.html.ep b/templates/history_map.html.ep
index 57ba81f..c2ff9ed 100644
--- a/templates/history_map.html.ep
+++ b/templates/history_map.html.ep
@@ -116,8 +116,8 @@
<div class="row">
<div class="col s12">
<p>
- Die eingezeichneten Routen stammen aus dem HAFAS und sind im Detail
- oft fehlerbehaftet.
+ Die eingezeichneten Routen stammen aus dem Backend, mit dem die Fahrt aufgezeichnet wurde.
+ Die Datenqualität variiert.
</p>
</div>
</div>
diff --git a/templates/journey.html.ep b/templates/journey.html.ep
index f5eebfc..31f9e94 100644
--- a/templates/journey.html.ep
+++ b/templates/journey.html.ep
@@ -15,29 +15,17 @@
<div class="col s12">
<p>
% if (my $name = stash('username')) {
- <b><a href="/p/<%= $name %>"><%= $name %></a></b>s
+ Checkin von <b><a href="/p/<%= $name %>"><%= $name %></a></b>
% }
- % if ($journey->{cancelled}) {
- Ausgefallene Fahrt
+ % elsif ($journey->{cancelled}) {
+ <b>Ausgefallene Fahrt</b> vom <%= $journey->{checkin}->strftime('%d.%m.%Y um %H:%M Uhr') %>
% }
% else {
- Fahrt
+ Checkin vom <%= $journey->{checkin}->strftime('%d.%m.%Y um %H:%M Uhr') %>
% }
% if ($journey->{edited} & 0x0020) {
% }
- von
- <b><%= $journey->{from_name} %></b>
- % if ($journey->{edited} & 0x0004) {
- ∗
- % }
- nach
- <b><%= $journey->{to_name} %></b>
- % if ($journey->{edited} & 0x0400) {
- ∗
- % }
- am
- <b><%= $journey->{sched_departure}->strftime('%d.%m.%Y') %></b>
% if (my $v = stash('journey_visibility')) {
% if (stash('username')) {
<i class="material-icons right"><%= visibility_icon($v) %></i>
@@ -78,19 +66,48 @@
</td>
</tr>
<tr>
+ <th scope="row">Von</th>
+ <td>
+ %= $journey->{from_name}
+ % if ($journey->{from_platform} and $journey->{to_platform}) {
+ (<%= $journey->{from_platform} %>)
+ % }
+ % if ($journey->{edited} & 0x0004) {
+ ∗
+ % }
+ </td>
+ </tr>
+ <tr>
+ <th scope="row">Nach</th>
+ <td>
+ <%= $journey->{to_name} %>
+ % if ($journey->{from_platform} and $journey->{to_platform}) {
+ (<%= $journey->{to_platform} %>)
+ % }
+ % if ($journey->{edited} & 0x0400) {
+ ∗
+ % }
+ </td>
+ </tr>
+ <tr>
<th scope="row">Abfahrt</th>
<td>
% if ($journey->{cancelled}) {
<i class="material-icons">cancel</i>
- (Plan: <%= $journey->{sched_departure}->strftime('%H:%M'); %>)
+ (Plan: <%= $journey->{sched_departure}->strftime('%d.%m.%Y %H:%M'); %>)
% }
- % elsif ($journey->{rt_departure} != $journey->{sched_departure}) {
- %= $journey->{rt_departure}->strftime('%H:%M');
- (<%= sprintf('%+d', ($journey->{rt_departure}->epoch - $journey->{sched_departure}->epoch) / 60) %>,
- Plan: <%= $journey->{sched_departure}->strftime('%H:%M'); %>)
+ % elsif ($journey->{delay_dep}) {
+ %= ($journey->{rt_departure}->epoch % 60) ? $journey->{rt_departure}->strftime('%d.%m.%Y %H:%M:%S') : $journey->{rt_departure}->strftime('%d.%m.%Y %H:%M')
+ % if (int(abs($journey->{delay_dep}) / 60)) {
+ (<%= sprintf('%+d', ($journey->{rt_departure}->epoch - $journey->{sched_departure}->epoch) / 60) %>, Plan:
+ % }
+ % else {
+ (Plan:
+ % }
+ %= ($journey->{sched_departure}->epoch % 60) ? $journey->{sched_departure}->strftime('%H:%M:%S)') : $journey->{sched_departure}->strftime('%H:%M)')
% }
% else {
- %= $journey->{sched_departure}->strftime('%H:%M');
+ %= ($journey->{sched_departure}->epoch % 60) ? $journey->{sched_departure}->strftime('%d.%m.%Y %H:%M:%S') : $journey->{sched_departure}->strftime('%d.%m.%Y %H:%M');
% }
% if ($journey->{edited} & 0x0003) {
@@ -103,19 +120,24 @@
% if ($journey->{cancelled}) {
<i class="material-icons">cancel</i>
% if ($journey->{sched_arrival}->epoch != 0) {
- (Plan: <%= $journey->{sched_arrival}->strftime('%H:%M'); %>)
+ (Plan: <%= $journey->{sched_arrival}->strftime('%d.%m.%Y %H:%M'); %>)
% }
% }
% elsif ($journey->{rt_arrival}->epoch == 0 and $journey->{sched_arrival}->epoch == 0) {
<i class="material-icons">timer_off</i>
% }
- % elsif ($journey->{rt_arrival} != $journey->{sched_arrival}) {
- %= $journey->{rt_arrival}->strftime('%H:%M');
- (<%= sprintf('%+d', ($journey->{rt_arrival}->epoch - $journey->{sched_arrival}->epoch) / 60) %>,
- Plan: <%= $journey->{sched_arrival}->strftime('%H:%M'); %>)
+ % elsif ($journey->{delay_arr}) {
+ %= ($journey->{rt_arrival}->epoch % 60) ? $journey->{rt_arrival}->strftime('%d.%m.%Y %H:%M:%S') : $journey->{rt_arrival}->strftime('%d.%m.%Y %H:%M')
+ % if (int(abs($journey->{delay_arr}) / 60)) {
+ (<%= sprintf('%+d', ($journey->{rt_arrival}->epoch - $journey->{sched_arrival}->epoch) / 60) %>, Plan:
+ % }
+ % else {
+ (Plan:
+ % }
+ %= ($journey->{sched_arrival}->epoch % 60) ? $journey->{sched_arrival}->strftime('%H:%M:%S)') : $journey->{sched_arrival}->strftime('%H:%M)')
% }
% else {
- %= $journey->{sched_arrival}->strftime('%H:%M');
+ %= ($journey->{sched_arrival}->epoch % 60) ? $journey->{sched_arrival}->strftime('%d.%m.%Y %H:%M:%S') : $journey->{sched_arrival}->strftime('%d.%m.%Y %H:%M');
% }
% if ($journey->{edited} & 0x0300) {
@@ -133,6 +155,9 @@
ca. <%= sprintf_km($journey->{km_route}) %>
(Luftlinie: <%= sprintf_km($journey->{km_beeline}) %>)
% }
+ % elsif ($journey->{km_beeline} > 0.1) {
+ (Luftlinie: <%= sprintf_km($journey->{km_beeline}) %>)
+ % }
% else {
?
% }
@@ -155,11 +180,22 @@
% }
% }
+ % elsif ($journey->{km_beeline} > 0.1 and $journey->{kmh_beeline} > 0.01) {
+ (<%= sprintf('%.f', $journey->{kmh_beeline}) %> km/h)
+ % }
% else {
?
% }
</td>
</tr>
+ % if ($journey->{user_data}{operator} or scalar @{ $journey->{user_data}{operators} // [] }) {
+ <tr>
+ <th scope="row">Betrieb</th>
+ <td>
+ %= $journey->{user_data}{operator} // join(q{, }, @{$journey->{user_data}{operators}})
+ </td>
+ </tr>
+ % }
% if ($journey->{messages} and @{$journey->{messages}}) {
<tr>
<th scope="row">Meldungen</th>
@@ -171,6 +207,16 @@
</td>
</tr>
% }
+ % if ($journey->{user_data}{him_msg} and @{$journey->{user_data}{him_msg}}) {
+ <tr>
+ <th scope="row">Meldungen</th>
+ <td>
+ % for my $message (@{$journey->{user_data}{him_msg} // []}) {
+ <i class="material-icons tiny"><%= ($message->{prio} and $message->{prio} eq 'HOCH') ? 'warning' : 'info' %></i> <%= $message->{header} %> <%= $message->{lead} %><br/>
+ % }
+ </td>
+ </tr>
+ % }
% if ($journey->{user_data} and $journey->{user_data}{comment}) {
<tr>
<th scope="row">Kommentar</th>
@@ -212,10 +258,10 @@
% my $within = 0;
% my $at_startstop = 0;
% for my $station (@{$journey->{route}}) {
- % if ($station->[0] eq $journey->{from_name}) {
+ % if (($station->[1] and $station->[1] == $journey->{from_eva}) or $station->[0] eq $journey->{from_name}) {
% $within = 1; $at_startstop = 1;
% }
- % elsif ($station->[0] eq $journey->{to_name}) {
+ % elsif (($station->[1] and $station->[1] == $journey->{to_eva}) or $station->[0] eq $journey->{to_name}) {
% $within = 0; $at_startstop = 1;
% }
% else {
@@ -248,7 +294,7 @@
% }
</span>
% }
- % if ($station->[0] eq $journey->{from_name}) {
+ % if (($station->[1] and $station->[1] == $journey->{from_eva}) or $station->[0] eq $journey->{from_name}) {
% $before = 0;
% }
<br/>
@@ -261,6 +307,13 @@
% if (stash('polyline_groups')) {
%= include '_map', station_coordinates => stash('station_coordinates'), polyline_groups => stash('polyline_groups')
% }
+ <div class="row">
+ <div class="col s12 grey-text">
+ <i class="material-icons tiny" aria-hidden="true"><%= $journey->{is_hafas} ? 'directions' : 'train' %></i>
+ %= $journey->{backend_name} || 'IRIS'
+ #<%= $journey->{id} %>
+ </div>
+ </div>
% if (not stash('readonly')) {
% if (stash('with_share')) {
<div class="row">
diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep
index 45bfb21..5ca0e9e 100644
--- a/templates/landingpage.html.ep
+++ b/templates/landingpage.html.ep
@@ -1,5 +1,6 @@
% if (is_user_authenticated()) {
% my $status = stash('user_status');
+ % my $user = stash('user');
% if (stash('error')) {
<div class="row">
<div class="col s12">
@@ -51,40 +52,63 @@
% if ( @{stash('timeline') // [] } ) {
%= include '_timeline_link', timeline => stash('timeline')
% }
- <div class="card">
- <div class="card-content">
- <span class="card-title">Hallo, <%= current_user->{name} %>!</span>
- <p>Du bist gerade nicht eingecheckt.</p>
- <div class="geolocation" data-recent="<%= join('|', map { $_->{eva} . ';' . $_->{name} . ';' . $_->{hafas} } @{stash('recent_targets') // []} ) %>">
- <button class="btn waves-effect waves-light btn-flat">Stationen in der Umgebung abfragen</button>
- </div>
- %= form_for 'list_departures' => begin
+ %= form_for 'list_departures' => begin
+ <div class="card">
+ <div class="card-content">
+ <span class="card-title">Hallo, <%= $user->{name} %>!</span>
+ <p>Du bist gerade nicht eingecheckt.</p>
+ <div class="geolocation" data-recent="<%= join('|', map { $_->{external_id_or_eva} . ';' . $_->{name} . ';' . $_->{dbris} . ';' . $_->{efa} . ';' . $_->{hafas} . ';' . $_->{motis} } @{stash('recent_targets') // []} ) %>" data-backend="<%= $user->{backend_id} %>">
+ <a class="btn waves-effect waves-light btn-flat request">Stationen in der Umgebung abfragen</a>
+ </div>
+ %= hidden_field backend_dbris => $user->{backend_dbris}
<div class="input-field">
%= text_field 'station', id => 'station', class => 'autocomplete contrast-color-text', autocomplete => 'off', required => undef
<label for="station">Manuelle Eingabe</label>
</div>
- <div class="center-align">
- <button class="btn waves-effect waves-light btn-flat" type="submit" name="action" value="departures">
- <i class="material-icons left" aria-hidden="true">send</i>
- Abfahrten
- </button>
- </div>
- %= end
+ </div>
+ <div class="card-action">
+ <a href="/account/select_backend?redirect_to=/" class="btn btn-flat"><i class="material-icons left" aria-hidden="true"><%= $user->{backend_hafas} ? 'directions' : 'train' %></i><%= $user->{backend_name} // 'IRIS' %></a>
+ <button class="btn right waves-effect waves-light btn-flat" type="submit" name="action" value="departures">
+ <i class="material-icons left" aria-hidden="true">send</i>
+ Abfahrten
+ </button>
+ </div>
</div>
- </div>
+ %= end
% }
</div>
</div>
+ % if (not $user->{backend_name}) {
+ <div class="row">
+ <div class="col s12">
+ <div class="card purple white-text">
+ <div class="card-content">
+ <span class="card-title">Legacy-Backend ausgewählt</span>
+ <p>
+ Das aktuell aktive IRIS-Backend wird nicht mehr weiterentwickelt und voraussichtlich bald von der Deutschen Bahn abgeschaltet.
+ Schon jetzt ist die Datenqualität wegen zunehmend schlechter Datenaufbereitungsmöglichkeiten oft unzureichend.
+ Das bahn.de-Backend ist in fast jeder Hinsicht besser geeignet; lediglich bei Verspätungs- und Servicemeldungen ist es geringfügig weniger detailliert und Checkin-Vorschläge werden derzeit nicht unterstützt.
+ </p>
+ </div>
+ <div class="card-action">
+ <a class="btn btn-flat" href="/account/select_backend?redirect_to=/">Backend wechseln</a>
+ </div>
+ </div>
+ </div>
+ </div>
+ % }
<h2 style="margin-left: 0.75rem;">Letzte Fahrten</h2>
- %= include '_history_trains', date_format => '%d.%m.%Y', journeys => [journeys->get(uid => current_user->{id}, limit => 5, with_datetime => 1)];
+ %= include '_history_trains', date_format => '%d.%m.%Y', journeys => [journeys->get(uid => $user->{id}, limit => 5, with_datetime => 1)];
% }
% else {
<div class="row">
<div class="col s12">
<p>
- Travelynx erlaubt das Einchecken in Züge im Netz der Deutschen
- Bahn. So können die eigenen Fahrten später inklusive Echtzeitdaten
- und eingetragenen Servicemeldungen nachvollzogen und brennende
+ Travelynx erlaubt das Einchecken in Verkehrsmittel (Busse,
+ Bahnen, Züge) unter anderem in Deutschland, Österreich, der
+ Schweiz, Luxemburg, Irland, Dänemark und Teilen der USA. So
+ können die eigenen Fahrten später inklusive Echtzeitdaten und
+ eingetragenen Servicemeldungen nachvollzogen und brennende
Fragen wie „Wie viele Stunden war ich letzten Monat unterwegs?“
beantwortet werden.
</p>
@@ -102,7 +126,7 @@
<li>Statistiken über Reisezeiten und Verspätungen</li>
<li>Unterstützung beim Ausfüllen von Fahrgastrechteformularen</li>
<li>Optional: Öffentlicher Reisestatus und öffentliche Angaben zu vergangenen Fahrten</li>
- <li>Optional: Verknüpfung mit Träwelling</li>
+ <!-- <li>Optional: Verknüpfung mit Träwelling</li> -->
</ul>
</p>
<p>
diff --git a/templates/layouts/default.html.ep b/templates/layouts/default.html.ep
index 80ef49e..b275335 100644
--- a/templates/layouts/default.html.ep
+++ b/templates/layouts/default.html.ep
@@ -13,7 +13,7 @@
% while (my ($key, $value) = each %{stash('opengraph') // {}}) {
<meta property="og:<%= $key %>" content="<%= $value %>">
% }
- % my $av = 'v71'; # asset version
+ % my $av = 'v97'; # asset version
<link rel="icon" type="image/png" href="/static/<%= $av %>/icons/icon-16x16.png" sizes="16x16">
<link rel="icon" type="image/png" href="/static/<%= $av %>/icons/icon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/static/<%= $av %>/icons/icon-96x96.png" sizes="96x96">
@@ -62,7 +62,7 @@
%= javascript "/static/${av}/js/geolocation${min}.js"
% }
% if (stash('with_autocomplete')) {
- %= javascript "/dyn/${av}/autocomplete.js", defer => undef
+ %= javascript "/dyn/${av}/autocomplete.js?backend_id=" . (stash('backend_id') // 1), defer => undef
% }
% if (stash('with_map')) {
%= javascript "/static/${av}/leaflet/leaflet.js"
diff --git a/templates/legend.html.ep b/templates/legend.html.ep
index 73fded9..3dc113a 100644
--- a/templates/legend.html.ep
+++ b/templates/legend.html.ep
@@ -15,11 +15,11 @@
</tr>
<tr>
<td><i class="material-icons">train</i></td>
- <td>Backend: DB IRIS. Bevorzugte Datenquelle für (mindestens teilweise) innerdeutsche Zugfahrten.</td>
+ <td>Backend: Deutsche Bahn (bahn.de oder IRIS-TTS).</td>
</tr>
<tr>
<td><i class="material-icons">directions</i></td>
- <td>Backend: DB HAFAS. Bevorzugte Datenquelle für Nahverkehr und vollständig außerdeutsche Zugfahrten. Weniger detailliert als IRIS.</td>
+ <td>Backend: HAFAS.</td>
</tr>
</tbody>
</table>
diff --git a/templates/login.html.ep b/templates/login.html.ep
index ce89813..3a9cc1f 100644
--- a/templates/login.html.ep
+++ b/templates/login.html.ep
@@ -74,6 +74,11 @@
</div>
</div>
<div class="row">
+ <div class="col s12 m12 l12">
+ Mit der Anmeldung stimmst du den <a href="/tos">Nutzungsbedingungen</a> zu.
+ </div>
+ </div>
+ <div class="row">
<div class="col s3 m3 l3">
</div>
<div class="col s6 m6 l6 center-align">
diff --git a/templates/passengerrights.html.ep b/templates/passengerrights.html.ep
index 3d5d21d..c189657 100644
--- a/templates/passengerrights.html.ep
+++ b/templates/passengerrights.html.ep
@@ -2,10 +2,10 @@
<div class="row">
<div class="col s12">
<p>
- Gemäß der Fahrgastrechte im Eisenbahnverkehr besteht ab 60 Minuten
- Verspätung am Ziel ein Entschädigungsanspruch gegenüber dem
- Eisenbahnverkehrsunternehmen. Dieser kann mit dem
- Fahrgastrechteformular geltend gemacht werden.
+ Ab 60 Minuten Verspätung am Ziel besteht in einigen Fällen ein
+ Entschädigungsanspruch gegenüber dem Eisenbahnverkehrsunternehmen.
+ Dieser kann mit dem Fahrgastrechteformular oder online geltend
+ gemacht werden.
</p>
<p>
Die folgenden Zugfahrten sind wahrscheinliche Kandidaten dafür.
@@ -73,3 +73,64 @@
</table>
</div>
</div>
+
+<div class="row">
+ <div class="col s12">
+ <p>
+ Bei Abo-Tickets besteht teilweise die Möglichkeit, bereits ab 20
+ Minuten Verspätung Fahrten gesammelt zu Entschädigungszwecken
+ einzureichen. Die folgenden Zugfahrten sind Kandidaten dafür.
+ Fahrten mit einer Verspätung von 60 Minuten oder mehr werden hier
+ nicht aufgeführt.
+ </p>
+ </div>
+</div>
+
+<div class="row">
+ <div class="col s12">
+ <table class="striped">
+ <thead>
+ <tr>
+ <th>Datum</th>
+ <th>Zug</th>
+ <th>Verspätung</th>
+ </tr>
+ </thead>
+ <tbody>
+ % for my $journey (@{$abo_journeys}) {
+ % my $detail_link = '/journey/' . $journey->{id};
+ <tr>
+ <td><%= $journey->{sched_departure}->strftime('%d.%m.%Y') %></td>
+ <td><a href="<%= $detail_link %>">
+ <%= $journey->{type} %> <%= $journey->{line} // $journey->{no} %>
+ → <%= $journey->{to_name} %>
+ % if ($journey->{connection}) {
+ % $detail_link = '/journey/' . $journey->{connection}{id};
+ </a><br/><a href="<%= $detail_link %>">
+ <%= $journey->{connection}{type} %> <%= $journey->{connection}{line} // $journey->{connection}{no} %>
+ → <%= $journey->{connection}{to_name} %>
+ % }
+ </a></td>
+ <td>
+ % if ($journey->{cancelled}) {
+ % if ($journey->{has_substitute}) {
+ Ausfall, Ersatzverbindung
+ %= sprintf('%+d', $journey->{substitute_delay})
+ % }
+ % else {
+ Ausfall ohne Ersatzverbindung
+ % }
+ % }
+ % elsif ($journey->{connection}) {
+ %= sprintf('%+d, ggf. Anschluss verpasst', $journey->{delay})
+ % }
+ % else {
+ %= sprintf('%+d', $journey->{delay})
+ % }
+ </td>
+ </tr>
+ % }
+ </tbody>
+ </table>
+ </div>
+</div>
diff --git a/templates/profile.html.ep b/templates/profile.html.ep
index 6f78ea0..a2c965c 100644
--- a/templates/profile.html.ep
+++ b/templates/profile.html.ep
@@ -79,7 +79,7 @@
</div>
<div class="row">
<div class="col s12 publicstatuscol" data-user="<%= $name %>" data-profile="1">
- %= include '_public_status_card', name => $name, privacy => $privacy, journey => $journey, from_profile => 1
+ %= include '_public_status_card', name => $name, privacy => $privacy, journey => $journey, from_profile => 1, station_coordinates => stash('station_coordinates'), polyline_groups => stash('polyline_groups')
</div>
</div>
% if ($journeys and @{$journeys}) {
diff --git a/templates/register.html.ep b/templates/register.html.ep
index ee344f9..f9a486a 100644
--- a/templates/register.html.ep
+++ b/templates/register.html.ep
@@ -27,6 +27,11 @@
</div>
</div>
<div class="row">
+ <div class="col s12 m12 l12">
+ Mit deiner Registrierung stimmst du den <a href="/tos">Nutzungsbedingungen</a> zu.
+ </div>
+ </div>
+ <div class="row">
<div class="col s3 m3 l3">
</div>
<div class="col s6 m6 l6 center-align">
diff --git a/templates/select_backend.html.ep b/templates/select_backend.html.ep
new file mode 100644
index 0000000..e3db44d
--- /dev/null
+++ b/templates/select_backend.html.ep
@@ -0,0 +1,85 @@
+<div class="row">
+ <div class="col s12">
+ <h2>Backend auswählen</h2>
+ <p style="text-align: justify;">
+ Das ausgewählte Backend bestimmt die Datenquelle für Fahrten in travelynx.
+ <a href="#help">Details</a>.
+ </p>
+ </div>
+</div>
+%= form_for '/account/select_backend' => (method => 'POST') => begin
+ % if (stash('redirect_to')) {
+ %= hidden_field 'redirect_to' => stash('redirect_to')
+ % }
+ % if (@{stash('suggestions') // []}) {
+ <div class="row">
+ <div class="col s12">
+ <h3>Vorschläge</h3>
+ <p style="text-align: justify;">
+ Anhand der Zielstation der letzten Fahrt und den
+ empfohlenen Nutzungsbereichen der verfügbaren Backends
+ (soweit bekannt).
+ </p>
+ </div>
+ </div>
+ % for my $backend (@{ stash('suggestions') // [] }) {
+ %= include '_backend_line', user => $user, backend => $backend
+ % }
+ % }
+ <div class="row">
+ <div class="col s12">
+ <h3>Empfohlen</h3>
+ <p style="text-align: justify;">
+ <strong>bahn.de</strong> für Regional- und Fernverkehr in Deutschland.
+ <strong>ÖBB</strong> für Nah-, Regional- und Fernverkehr in Österreich sowie Regional- und Fernverkehr in der EU.
+ </p>
+ </div>
+ </div>
+ % for my $backend (grep { $_->{recommended} } @{ stash('backends') // [] }) {
+ %= include '_backend_line', user => $user, backend => $backend
+ % }
+ <div class="row">
+ <div class="col s12">
+ <h3>Verbünde</h3>
+ <p style="text-align: justify;">
+ Diese Backends sind meist die beste Wahl für
+ Nahverkehrsfahrten in der jeweiligen Region.
+ Backends außerhalb Deutschlands sind im Regelfall auch
+ für dortigen Regional- und Fernverkehr die beste Wahl.
+ </p>
+ </div>
+ </div>
+ % for my $backend (grep { $_->{association} } @{ stash('backends') // [] }) {
+ %= include '_backend_line', user => $user, backend => $backend
+ % }
+ <div class="row">
+ <div class="col s12">
+ <h3>Experimentell oder abgekündigt</h3>
+ <p style="text-align: justify;">
+ Einchecken auf eigene Gefahr.
+ </p>
+ </div>
+ </div>
+ % for my $backend (grep { $_->{experimental} or $_->{legacy} } @{ stash('backends') // [] }) {
+ %= include '_backend_line', user => $user, backend => $backend
+ % }
+%= end
+<div class="row">
+ <div class="col s12">
+ <h2 id="help">Details</h2>
+ <p>
+ <strong>Deutsche Bahn: bahn.de</strong> ist eine gute Wahl für Fahrten des Nah-, Regional- und Fernverkehrs innerhalb Deutschlands.
+ Dieses Backend bietet überwiegend korrekte Echtzeit- und Kartendaten sowie Wagenreihungen.
+ Bei Nahverkehrsfahrten sind die Echtzeit- und Kartendaten meist nicht so gut wie bei den APIs des jeweiligen Verkehrsverbunds.
+ <p>
+ <strong>ÖBB</strong> liefern Kartendaten und Wagenreihungen für Fernverkehr in Deutschland und Umgebung, jedoch keine Meldungen. Echtzeitdaten sind teilweise verfügbar.
+ </p>
+ <p>
+ <strong>Deutsche Bahn: IRIS-TTS</strong> liefert Echtzeitdaten (nur am Start- und Zielbahnhof), Wagenreihungen und Verspätungsmeldungen für Regional- und Fernverkehr in Deutschland. Kartendaten und Angaben zu Unterwegshalten sind nur teilweise verfügbar. Dieses Backend wird nicht mehr weiterentwickelt. Die zugehörige API wird voraussichtlich im Laufe des Jahres 2025 abgeschaltet.
+ </p>
+ <p>
+ <strong>Transitous</strong> ist ein Aggregator für eine Vielzahl von Verkehrsunternehmen.
+ Die Datenqualität variiert.
+ </p>
+ </div>
+</div>
diff --git a/templates/traewelling.html.ep b/templates/traewelling.html.ep
index c1f2b7d..49b5c80 100644
--- a/templates/traewelling.html.ep
+++ b/templates/traewelling.html.ep
@@ -41,30 +41,6 @@
</div>
% }
-<div class="row">
- <div class="col s12">
- <div class="card purple">
- <div class="card-content white-text">
- <span class="card-title">Eingeschränkte Synchronisierung</span>
- <p>
- Träwelling und travelynx setzen unterschiedliche Schwerpunkte und haben unterschiedliche Features.
- Kombiniert mit der Vielzahl an möglichen Randfällen heißt das, dass die Synchronisierung nicht immer funktioniert.
- Diese Einschränkung ist bekannt und wird voraussichtlich bestehen bleiben.
- </p>
- <p>
- Bei hohen Verspätungen, Ausfällen und nachträglichen Checkin-Änderungen ist die Synchronisierung u.U. nicht möglich und muss von Hand vorgenommen werden.
- travelynx-Hooks werden bei via Träwelling vorgenommenen Checkins nicht ausgelöst.
- </p>
- </div>
- <div class="card-action">
- <a href="https://github.com/derf/travelynx/issues" class="waves-effect waves-light btn-flat white-text">
- <i class="material-icons left" aria-hidden="true">bug_report</i>Bug melden
- </a>
- </div>
- </div>
- </div>
-</div>
-
% if ($traewelling->{token} and ($traewelling->{expired} or $traewelling->{expiring})) {
<div class="row">
<div class="col s12">
@@ -157,26 +133,21 @@
<div>
<label>
%= check_box toot => 1
- <span>… Checkin auf Mastodon veröffentlichen</span>
- </label>
- </div>
- <div>
- <label>
- %= check_box tweet => 1
- <span>… Checkin auf Twitter veröffentlichen</span>
+ <span>… Checkin im Fediverse veröffentlichen</span>
</label>
</div>
<p>Die Synchronisierung erfolgt spätestens drei Minuten nach der
- Zielwahl. Beachte, dass die Synchronisierung travelynx
- → Träwelling unabhängig von der eingestellten Sichtbarkeit
- des Checkins erfolgt. travelynx reicht die Sichtbarkeit
- aber an Träwelling weiter.
- Träwelling-Checkins können von travelynx aktuell nicht
- rückgängig gemacht werden. Eine nachträgliche Änderung der
- Zielstation wird nicht übernommen. Mastodon und Twitter beziehen
- sich auf die in den <a
+ Zielwahl. Es werden ausschließlich Checkins mittels
+ DB (IRIS-TTS) und DB (HAFAS) synchornisiert. Beachte, dass
+ die Synchronisierung travelynx → Träwelling unabhängig von
+ der eingestellten Sichtbarkeit des Checkins erfolgt.
+ travelynx reicht die Sichtbarkeit aber an Träwelling
+ weiter. Träwelling-Checkins können von travelynx aktuell
+ nicht rückgängig gemacht werden. Eine nachträgliche
+ Änderung der Zielstation wird nicht übernommen. Fediverse
+ bezieht sich auf den in den <a
href="https://traewelling.de/settings">Träwelling-Einstellungen</a>
- verknüpften Accounts.</p>
+ verknüpften Account.</p>
</div>
<div class="input-field col s12">
<div>
diff --git a/templates/use_external_links.html.ep b/templates/use_external_links.html.ep
deleted file mode 100644
index d7bebd7..0000000
--- a/templates/use_external_links.html.ep
+++ /dev/null
@@ -1,82 +0,0 @@
-<h1>Externe Dienste</h1>
-<div class="row">
- <div class="col s12">
- <p>
- Travelynx kann an geeigneten Stellen Links zu externen Diensten
- (z.B. Abfahrstafeln oder Informationen zum gerade genutzten Zug)
- einbinden. Hier lässt sich konfigurieren, welcher Dienst für welche
- Art von Informationen genutzt wird.
- <p/>
- </div>
-</div>
-<h2>Abfahrtstafel</h2>
-%= form_for '/account/services' => (method => 'POST') => begin
- %= csrf_field
- <div class="row">
- <div class="col s12">
- Angaben zu anderen an einer Station verkehrenden Verkehrsmitteln
- </div>
- </div>
- <div class="row">
- <div class="input-field col s12">
- <div>
- <label>
- %= radio_button stationboard => '0'
- <span>Keine</span>
- </label>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="input-field col s12">
- <div>
- <label>
- %= radio_button stationboard => '1'
- <span><a href="https://dbf.finalrewind.org/">DBF</a> (Schienenverkehr)</span>
- </label>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="input-field col s12">
- <div>
- <label>
- %= radio_button stationboard => '2'
- <span><a href="https://bahn.expert/">bahn.expert</a> (Schienenverkehr)</span>
- </label>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="input-field col s12">
- <div>
- <label>
- %= radio_button stationboard => '3'
- <span><a href="https://dbf.finalrewind.org/?hafas=1">DBF</a> (Nahverkehr)</span>
- </label>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="input-field col s12">
- <div>
- <label>
- %= radio_button stationboard => '4'
- <span><a href="https://bahn.expert/regional">bahn.expert/regional</a> (Nahverkehr)</span>
- </label>
- </div>
- </div>
- </div>
- <div class="row">
- <div class="col s3 m3 l3">
- </div>
- <div class="col s6 m6 l6 center-align">
- <button class="btn waves-effect waves-light" type="submit" name="action" value="save">
- Speichern
- <i class="material-icons right">send</i>
- </button>
- </div>
- <div class="col s3 m3 l3">
- </div>
- </div>
-%= end
diff --git a/templates/use_history.html.ep b/templates/use_history.html.ep
index 9b76e98..f91ca16 100644
--- a/templates/use_history.html.ep
+++ b/templates/use_history.html.ep
@@ -5,9 +5,13 @@
Travelynx kann anhand deiner vergangenen Fahrten Verbindungen zum
Einchecken vorschlagen. Fährst zu z.B regelmäßig von Dortmund Hbf
nach Essen Hbf, werden dir in Dortmund bevorzugt Fahrten angezeigt, die
- Essen passieren. Bei Auswahl dieser wird nicht nur in die Fahrt eingecheckt,
+ Essen passieren. Bei Auswahl dieser wird nicht nur in die Fahrt eingecheckt,
sondern auch direkt Essen Hbf als Ziel eingetragen.
<p/>
+ <p>
+ Beachte, dass nicht alle von travelynx unterstützten Backends die
+ für dieses Feature notwendigen Daten bereitstellen.
+ </p>
<!-- <p>
Falls du das nicht nützlich findest oder nicht möchtest, dass deine
regelmäßigen (Anschluss-)Züge auf deinem Bildschirm sichtbar sind,
diff --git a/templates/user_status.html.ep b/templates/user_status.html.ep
index 45fba54..bf11004 100644
--- a/templates/user_status.html.ep
+++ b/templates/user_status.html.ep
@@ -1,5 +1,5 @@
<div class="row">
<div class="col s12 publicstatuscol" data-user="<%= $name %>">
- %= include '_public_status_card', name => $name, privacy => $privacy, journey => $journey
+ %= include '_public_status_card', name => $name, privacy => $privacy, journey => $journey, station_coordinates => stash('station_coordinates'), polyline_groups => stash('polyline_groups')
</div>
</div>
diff --git a/update.sh b/update.sh
index d78f8a5..58e3663 100755
--- a/update.sh
+++ b/update.sh
@@ -12,9 +12,11 @@ if [ "$1" = "with-deps" ]; then
carton install
cd ..
sudo systemctl stop travelynx
+ touch maintenance
mv local local.old
mv local.new/local .
perl index.pl database migrate
+ rm -f maintenance
sudo systemctl start travelynx
elif perl index.pl database has-current-schema; then
sudo systemctl reload travelynx