diff options
91 files changed, 9456 insertions, 1649 deletions
diff --git a/.github/workflows/perl.yml b/.github/workflows/perl.yml index 12a38d0..58eac2b 100644 --- a/.github/workflows/perl.yml +++ b/.github/workflows/perl.yml @@ -36,6 +36,8 @@ jobs: steps: - uses: actions/checkout@v2 + with: + submodules: 'true' - name: perl -V run: perl -V - name: Setup Repo diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a144be7 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ext/dbdb"] + path = ext/dbdb + url = https://github.com/derf/train-platform-directions.git @@ -3,193 +3,24 @@ travelynx - Railway Travel Logger [travelynx](https://finalrewind.org/projects/travelynx/) allows checking into 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 -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. +most of Germany, Switzerland, Austria, Luxembourg, Ireland, Denmark, 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 +long-distance, regional and local transit exposed by the Deutsche Bahn [bahn.de +interface](https://finalrewind.org/projects/Travel-Status-DE-DBRIS/), a variety +of [EFA](https://finalrewind.org/projects/Travel-Status-DE-VRR/) and +[HAFAS](https://finalrewind.org/projects/Travel-Status-DE-HAFAS/) interfaces, +andt [MOTIS](https://finalrewind.org/projects/Travel-Status-MOTIS/) APIs +including the [transitous](https://transitous.org/) aggregator. You can use the public instance on [travelynx.de](https://travelynx.de) or -host your own. See the Installation and Setup notes below. +host your own. Further reading: -Dependencies ---- - - * perl ≥ 5.20 - * carton - * build-essential - * libpq-dev - * git - -Installation ---- - -travelynx depends on a set of Perl modules which are documented in `cpanfile`. -After installing the dependencies mentioned above, you can use carton to -install Perl depenencies locally. You may alsobe able to use cpanminus; -however this method is untested. - -In the project root directory (where `cpanfile` resides), run - -``` -carton install --deployment -``` - -and set `PERL5LIB=.../local/lib/perl5` before executing any travelynx -commands (see configs in the examples directory) or wrap them with `carton -exec`, e.g. `carton exec hypnotoad index.pl` - -Setup ---- - -First, you need to set up a PostgreSQL database so that travelynx can store -user accounts and journeys. It must be at least version 9.4 and must use a -UTF-8 locale. The following steps describe setup on a Debian 9 system; -setup on other distributions should be similar. - -* Write down a strong random password -* Create a postgres user for travelynx: `sudo -u postgres createuser -P travelynx` - (enter password when prompted) -* Create the database: `sudo -u postgres createdb -O travelynx travelynx` -* Copy `examples/travelynx.conf` to the application root directory - (the one in which `index.pl` resides) and edit it. Make sure to configure - db, cache, mail, and secrets. -* Initialize the database: `carton exec perl index.pl database migrate` - or `PERL5LIB=local/lib/perl5 perl index.pl database migrate` +* [Contributing](doc/contributing.md) to travelynx development +* [Setup](doc/setup.md) for hosting your own instance +* [Usage](doc/usage.md) primer (what is this whole “checking in” about?) -Your server also needs to be able to send mail. Set up your MTA of choice and -make sure that the sendmail binary can be used for outgoing mails. Mail -reception on the server is not required. - -Finally, configure the web service: - -* Set up a travelynx service using the service supervisor of your choice - (see `examples/travelynx.service` for a systemd unit file) -* Configure your web server to reverse-provy requests to the travelynx - instance. See `examples/nginx-site` for an nginx config. -* Install a `timeout 5m perl index.pl work -m production` cronjob. It is used - to update realtime data and perform automatic checkout and should run - every three minutes or so, see `examples/cron`. - -You can now start the travelynx service, navigate to the website and register -your first account. There is no admin account, all management is performed -via cron or (in non-standard cases) on the command line. - -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 ---- - -It is recommended to run travelynx directly from the git repository. When -updating, the workflow depends on whether schema updates need to be applied -or not. - -``` -git pull -carton install --deployment # if you are using carton: update dependencies -chmod -R a+rX . # only needed if travelynx is running under a different user -if perl index.pl database has-current-schema; then - systemctl reload travelynx -else - systemctl stop travelynx - perl index.pl database migrate - systemctl start travelynx -fi -``` - -Note that this is subject to change -- the application may perform schema -updates automatically in the future. If you used carton for installation, -use `carton exec perl ...` in the snippet above; otherwise, export -`PERL5LIB=.../local/lib/perl5`. - -Setup with Docker ---- - -Note that travelynx Docker support is experimental and, in its current form, -far from best practices. Pull requests are appreciated. - -First, you need to set up a PostgreSQL database so that travelynx can store -user accounts and journeys. It must be at least version 9.4 and must use a -UTF-8 locale. See above (or `examples/docker/postgres-init.sh`) for database -initialization. You do not need to perform the `database migrate` step. - -Next, you need to prepare three files that will be mounted into the travelynx -container: travelynx configuration, e-mail configuration, and imprint and -privacy policy. For the sake of this readme, we assume that you are using the -`local/` directory to store these - -* `mkdir local` -* copy examples/travelynx.conf to local/travelynx.conf and configure it. -* copy examples/docker/email-transport.sh to local/email-transport.sh and configure it. - 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. - -travelynx consists of two runtimes: the web application and a background -worker. Your service supervisor (or docker compose / docker stack / kubernetes -setup) should orchestrate them somewhere along these lines. - -* `docker pull derfnull/travelynx:latest` -* Start web application: `docker run -p 8093:8093 -v ${PWD}/local:/local:ro travelynx:latest` -* Wait until localhost:8093 responds to requests -* Start worker: `docker run -v ${PWD}/local:/local:ro travelynx:latest worker` - -To install an update: stop worker and web application, update the travelynx -image, and start them again. Database migrations will be performed -automatically. Note that downgrades are not supported. - -Usage ---- - -For the sake of this manual, we will assume your travelynx instance is running -on `travelynx.de` - -travelynx journey logging is based on checkin and checkout actions: You check -into a train when boarding it, select a destination, and are automatically -checked out when you arrive. Real-time data is saved on both occasions and -continuously updated while in transit, providing an accurate overview of both -scheduled and actual journey times. - -## Checking in - -You can check into a train up to 30 minutes before its scheduled departure and -up to two hours after its actual departure (including delays). - -First, you need to select the station you want to check in from. -Navigate to `travelynx.de` or click/tap on the travelynx text in the navigation -bar. You will see a list of the five stations closest to your current location -(as reported by your browser). Select the station you're at or enter its -name or DS100 code manually. - -As soon as you select a train, you will be checked in and travelynx will switch -to the journey / checkout view. If you already know where you're headed, you -should click/tap on the destination station in the station list now. You can -change the destination by selecting a new one anytime. - -## Checking out - -You are automatically checked out a few minutes after arrival at your -destination. If the train has already arrived when you select a destination and -its arrival was less than two hours ago, you are checked out immediately. If -it's more than two hours, you need to perform a manual checkout (without -arrival data) using the link at the bottom of the checkin menu's station list. - -Testing ---- +## Testing The test scripts assume that travelynx.conf contains a valid database connection. They will create a test-specific schema, perform all operations in @@ -202,8 +33,7 @@ Please use a separate development database instead. Run the tests by executing `prove`. Use `prove -v` for debug output and `DBI_TRACE=SQL prove -v` to monitor SQL queries. -Licensing ---- +## Licensing The copyright of individual files is documented in the file's header or in .reuse/dep5. The referenced licenses are stored in the LICENSES directory. @@ -223,11 +53,11 @@ The easiest way of making changes available is by maintaining a public fork of the Git repository. A tarball is also acceptable. Please change the `source` ref in travelynx.conf if you are using a fork with custom changes. -References ---- +## References Mirrors of the travelynx repository are maintained at the following locations: * [Chaosdorf](https://chaosdorf.de/git/derf/travelynx) +* [Codeberg](https://codeberg.org/derf/travelynx) * [git.finalrewind.org](https://git.finalrewind.org/travelynx/) * [GitHub](https://github.com/derf/travelynx) @@ -8,7 +8,11 @@ requires 'GIS::Distance'; requires 'GIS::Distance::Fast'; requires 'IO::Socket::Socks', '>= 0.64'; requires 'IO::Socket::SSL', '>= 2.009'; +requires 'JSON'; +requires 'JSON::XS'; requires 'List::UtilsBy'; +requires 'Locale::Maketext'; +requires 'Locale::Maketext::Lexicon'; requires 'Math::Polygon'; requires 'MIME::Entity'; requires 'Mojolicious'; @@ -17,11 +21,10 @@ requires 'Mojolicious::Plugin::OAuth2'; requires 'Mojo::Pg'; requires 'Text::CSV'; requires 'Text::Markdown'; -requires 'Travel::Status::DE::EFA', '>= 3.12'; +requires 'Travel::Status::DE::EFA', '>= 3.13'; requires 'Travel::Status::MOTIS', '>= 0.01'; -requires 'Travel::Status::DE::DBRIS', '>= 0.10'; +requires 'Travel::Status::DE::DBRIS', '>= 0.17'; requires 'Travel::Status::DE::HAFAS', '>= 6.20'; requires 'Travel::Status::DE::IRIS'; requires 'UUID::Tiny'; -requires 'JSON'; -requires 'JSON::XS'; +requires 'XML::LibXML'; diff --git a/cpanfile.snapshot b/cpanfile.snapshot index e68a6e6..975c46c 100644 --- a/cpanfile.snapshot +++ b/cpanfile.snapshot @@ -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: @@ -974,6 +985,14 @@ DISTRIBUTIONS perl 5.008004 strict 0 warnings 0 + Devel-GlobalDestruction-0.14 + pathname: H/HA/HAARG/Devel-GlobalDestruction-0.14.tar.gz + provides: + Devel::GlobalDestruction 0.14 + requirements: + ExtUtils::MakeMaker 0 + Sub::Exporter::Progressive 0.001011 + perl 5.006 Devel-StackTrace-2.05 pathname: D/DR/DROLSKY/Devel-StackTrace-2.05.tar.gz provides: @@ -1197,10 +1216,10 @@ DISTRIBUTIONS Text::ParseWords 3.24 strict 0 warnings 0 - ExtUtils-InstallPaths-0.014 - pathname: L/LE/LEONT/ExtUtils-InstallPaths-0.014.tar.gz + ExtUtils-InstallPaths-0.015 + pathname: L/LE/LEONT/ExtUtils-InstallPaths-0.015.tar.gz provides: - ExtUtils::InstallPaths 0.014 + ExtUtils::InstallPaths 0.015 requirements: Carp 0 ExtUtils::Config 0.009 @@ -1401,19 +1420,19 @@ DISTRIBUTIONS Time::Zone 0 perl 5.006002 strict 0 - 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 + HTTP-Message-7.01 + pathname: O/OA/OALDERS/HTTP-Message-7.01.tar.gz + provides: + HTTP::Config 7.01 + HTTP::Headers 7.01 + HTTP::Headers::Auth 7.01 + HTTP::Headers::ETag 7.01 + HTTP::Headers::Util 7.01 + HTTP::Message 7.01 + HTTP::Request 7.01 + HTTP::Request::Common 7.01 + HTTP::Response 7.01 + HTTP::Status 7.01 requirements: Carp 0 Clone 0.46 @@ -1483,18 +1502,18 @@ DISTRIBUTIONS Exporter 5.57 ExtUtils::MakeMaker 0 perl 5.008 - IO-Socket-SSL-2.093 - pathname: S/SU/SULLR/IO-Socket-SSL-2.093.tar.gz + IO-Socket-SSL-2.095 + pathname: S/SU/SULLR/IO-Socket-SSL-2.095.tar.gz provides: - IO::Socket::SSL 2.093 + IO::Socket::SSL 2.095 IO::Socket::SSL::Intercept 2.056 - IO::Socket::SSL::OCSP_Cache 2.093 - IO::Socket::SSL::OCSP_Resolver 2.093 + IO::Socket::SSL::OCSP_Cache 2.095 + IO::Socket::SSL::OCSP_Resolver 2.095 IO::Socket::SSL::PublicSuffix undef - IO::Socket::SSL::SSL_Context 2.093 - IO::Socket::SSL::SSL_HANDLE 2.093 - IO::Socket::SSL::Session_Cache 2.093 - IO::Socket::SSL::Trace 2.093 + IO::Socket::SSL::SSL_Context 2.095 + IO::Socket::SSL::SSL_HANDLE 2.095 + IO::Socket::SSL::Session_Cache 2.095 + IO::Socket::SSL::Trace 2.095 IO::Socket::SSL::Utils 2.015 requirements: ExtUtils::MakeMaker 0 @@ -1528,10 +1547,10 @@ DISTRIBUTIONS requirements: ExtUtils::MakeMaker 0 Test::More 0 - JSON-XS-4.03 - pathname: M/ML/MLEHMANN/JSON-XS-4.03.tar.gz + JSON-XS-4.04 + pathname: M/ML/MLEHMANN/JSON-XS-4.04.tar.gz provides: - JSON::XS 4.03 + JSON::XS 4.04 requirements: Canary::Stability 0 ExtUtils::MakeMaker 6.52 @@ -1605,6 +1624,72 @@ DISTRIBUTIONS requirements: Exporter 5.57 Module::Build 0.4004 + Locale-Maketext-Lexicon-1.00 + pathname: D/DR/DRTECH/Locale-Maketext-Lexicon-1.00.tar.gz + provides: + Locale::Maketext::Extract 1.00 + Locale::Maketext::Extract::Plugin::Base 1.00 + Locale::Maketext::Extract::Plugin::FormFu 1.00 + Locale::Maketext::Extract::Plugin::FormFu::Extractor 1.00 + Locale::Maketext::Extract::Plugin::Generic 1.00 + Locale::Maketext::Extract::Plugin::Haml 1.00 + Locale::Maketext::Extract::Plugin::Mason 1.00 + Locale::Maketext::Extract::Plugin::PPI 1.00 + Locale::Maketext::Extract::Plugin::Perl 1.00 + Locale::Maketext::Extract::Plugin::TT2 1.00 + Locale::Maketext::Extract::Plugin::TT2::Directive 1.00 + Locale::Maketext::Extract::Plugin::TT2::Parser 1.00 + Locale::Maketext::Extract::Plugin::TextTemplate 1.00 + Locale::Maketext::Extract::Plugin::TextTemplate::Parser 1.00 + Locale::Maketext::Extract::Plugin::YAML 1.00 + Locale::Maketext::Extract::Plugin::YAML::Extractor 1.00 + Locale::Maketext::Extract::Run 1.00 + Locale::Maketext::Lexicon 1.00 + Locale::Maketext::Lexicon::Auto 1.00 + Locale::Maketext::Lexicon::Gettext 1.00 + Locale::Maketext::Lexicon::Msgcat 1.00 + Locale::Maketext::Lexicon::Tie 1.00 + requirements: + ExtUtils::MakeMaker 6.30 + Locale::Maketext 1.17 + Log-Report-1.42 + pathname: M/MA/MARKOV/Log-Report-1.42.tar.gz + provides: + Log::Report 1.42 + Log::Report::DBIC::Profiler 1.42 + Log::Report::Die 1.42 + Log::Report::Dispatcher 1.42 + Log::Report::Dispatcher::Callback 1.42 + Log::Report::Dispatcher::File 1.42 + Log::Report::Dispatcher::Log4perl 1.42 + Log::Report::Dispatcher::LogDispatch 1.42 + Log::Report::Dispatcher::Perl 1.42 + Log::Report::Dispatcher::Syslog 1.42 + Log::Report::Dispatcher::Try 1.42 + Log::Report::Domain 1.42 + Log::Report::Exception 1.42 + Log::Report::Message 1.42 + Log::Report::Translator 1.42 + MojoX::Log::Report 1.42 + requirements: + Devel::GlobalDestruction 0.09 + Encode 2.00 + ExtUtils::MakeMaker 0 + Log::Report::Optional 1.07 + Scalar::Util 0 + String::Print 0.91 + Sys::Syslog 0.27 + Log-Report-Optional-1.08 + pathname: M/MA/MARKOV/Log-Report-Optional-1.08.tar.gz + provides: + Log::Report::Minimal 1.08 + Log::Report::Minimal::Domain 1.08 + Log::Report::Optional 1.08 + Log::Report::Util 1.08 + requirements: + ExtUtils::MakeMaker 0 + String::Print 0.91 + Test::More 0.86 MIME-Base32-1.303 pathname: R/RE/REHSACK/MIME-Base32-1.303.tar.gz provides: @@ -1614,6 +1699,16 @@ DISTRIBUTIONS ExtUtils::MakeMaker 0 perl 5.008001 utf8 0 + MIME-Charset-1.013.1 + pathname: N/NE/NEZUMI/MIME-Charset-1.013.1.tar.gz + provides: + MIME::Charset v1.13.1 + requirements: + CPAN 0 + Encode 1.98 + ExtUtils::MakeMaker 6.42 + Test::More 0 + perl 5.005 MIME-tools-5.515 pathname: D/DS/DSKOLL/MIME-tools-5.515.tar.gz provides: @@ -1701,17 +1796,18 @@ DISTRIBUTIONS Net::Domain 1.05 Net::SMTP 1.28 Test::More 0 - Math-Polygon-1.11 - pathname: M/MA/MARKOV/Math-Polygon-1.11.tar.gz + Math-Polygon-2.00 + pathname: M/MA/MARKOV/Math-Polygon-2.00.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 + Math::Polygon 2.00 + Math::Polygon::Calc 2.00 + Math::Polygon::Clip 2.00 + Math::Polygon::Convex 2.00 + Math::Polygon::Surface 2.00 + Math::Polygon::Transform 2.00 requirements: ExtUtils::MakeMaker 0 + Log::Report 0 Math::Trig 0 Scalar::Util 1.13 Test::More 0.47 @@ -1820,10 +1916,10 @@ DISTRIBUTIONS requirements: ExtUtils::MakeMaker 0 perl 5.006000 - Mojo-Pg-4.27 - pathname: S/SR/SRI/Mojo-Pg-4.27.tar.gz + Mojo-Pg-4.28 + pathname: S/SR/SRI/Mojo-Pg-4.28.tar.gz provides: - Mojo::Pg 4.27 + Mojo::Pg 4.28 Mojo::Pg::Database undef Mojo::Pg::Migrations undef Mojo::Pg::PubSub undef @@ -1835,8 +1931,8 @@ DISTRIBUTIONS Mojolicious 8.50 SQL::Abstract::Pg 1.0 perl 5.016 - Mojolicious-9.40 - pathname: S/SR/SRI/Mojolicious-9.40.tar.gz + Mojolicious-9.42 + pathname: S/SR/SRI/Mojolicious-9.42.tar.gz provides: Mojo undef Mojo::Asset undef @@ -1883,6 +1979,7 @@ DISTRIBUTIONS Mojo::Reactor undef Mojo::Reactor::EV undef Mojo::Reactor::Poll undef + Mojo::SSE undef Mojo::Server undef Mojo::Server::CGI undef Mojo::Server::Daemon undef @@ -1905,7 +2002,7 @@ DISTRIBUTIONS Mojo::UserAgent::Transactor undef Mojo::Util undef Mojo::WebSocket undef - Mojolicious 9.40 + Mojolicious 9.42 Mojolicious::Command undef Mojolicious::Command::Author::cpanify undef Mojolicious::Command::Author::generate undef @@ -2007,13 +2104,13 @@ DISTRIBUTIONS requirements: ExtUtils::MakeMaker 0 Module::Runtime 0.014 - Net-HTTP-6.23 - pathname: O/OA/OALDERS/Net-HTTP-6.23.tar.gz + Net-HTTP-6.24 + pathname: O/OA/OALDERS/Net-HTTP-6.24.tar.gz provides: - Net::HTTP 6.23 - Net::HTTP::Methods 6.23 - Net::HTTP::NB 6.23 - Net::HTTPS 6.23 + Net::HTTP 6.24 + Net::HTTP::Methods 6.24 + Net::HTTP::NB 6.24 + Net::HTTPS 6.24 requirements: Carp 0 Compress::Raw::Zlib 0 @@ -2134,11 +2231,11 @@ DISTRIBUTIONS overload 0 strict 0 warnings 0 - Path-Tiny-0.148 - pathname: D/DA/DAGOLDEN/Path-Tiny-0.148.tar.gz + Path-Tiny-0.150 + pathname: D/DA/DAGOLDEN/Path-Tiny-0.150.tar.gz provides: - Path::Tiny 0.148 - Path::Tiny::Error 0.148 + Path::Tiny 0.150 + Path::Tiny::Error 0.150 requirements: Carp 0 Cwd 0 @@ -2212,53 +2309,56 @@ DISTRIBUTIONS ExtUtils::MakeMaker 0 SQL::Abstract 2.0 perl 5.016 - Specio-0.50 - pathname: D/DR/DROLSKY/Specio-0.50.tar.gz - provides: - Specio 0.50 - Specio::Coercion 0.50 - Specio::Constraint::AnyCan 0.50 - Specio::Constraint::AnyDoes 0.50 - Specio::Constraint::AnyIsa 0.50 - Specio::Constraint::Enum 0.50 - Specio::Constraint::Intersection 0.50 - Specio::Constraint::ObjectCan 0.50 - Specio::Constraint::ObjectDoes 0.50 - Specio::Constraint::ObjectIsa 0.50 - Specio::Constraint::Parameterizable 0.50 - Specio::Constraint::Parameterized 0.50 - Specio::Constraint::Role::CanType 0.50 - Specio::Constraint::Role::DoesType 0.50 - Specio::Constraint::Role::Interface 0.50 - Specio::Constraint::Role::IsaType 0.50 - Specio::Constraint::Simple 0.50 - Specio::Constraint::Structurable 0.50 - Specio::Constraint::Structured 0.50 - Specio::Constraint::Union 0.50 - Specio::Declare 0.50 - Specio::DeclaredAt 0.50 - Specio::Exception 0.50 - Specio::Exporter 0.50 - Specio::Helpers 0.50 - Specio::Library::Builtins 0.50 - Specio::Library::Numeric 0.50 - Specio::Library::Perl 0.50 - Specio::Library::String 0.50 - Specio::Library::Structured 0.50 - Specio::Library::Structured::Dict 0.50 - Specio::Library::Structured::Map 0.50 - Specio::Library::Structured::Tuple 0.50 - Specio::OO 0.50 - Specio::PartialDump 0.50 - Specio::Registry 0.50 - Specio::Role::Inlinable 0.50 - Specio::Subs 0.50 - Specio::TypeChecks 0.50 - Test::Specio 0.50 + Specio-0.53 + pathname: D/DR/DROLSKY/Specio-0.53.tar.gz + provides: + Specio 0.53 + Specio::Coercion 0.53 + Specio::Constraint::AnyCan 0.53 + Specio::Constraint::AnyDoes 0.53 + Specio::Constraint::AnyIsa 0.53 + Specio::Constraint::Enum 0.53 + Specio::Constraint::Intersection 0.53 + Specio::Constraint::ObjectCan 0.53 + Specio::Constraint::ObjectDoes 0.53 + Specio::Constraint::ObjectIsa 0.53 + Specio::Constraint::Parameterizable 0.53 + Specio::Constraint::Parameterized 0.53 + Specio::Constraint::Role::CanType 0.53 + Specio::Constraint::Role::DoesType 0.53 + Specio::Constraint::Role::Interface 0.53 + Specio::Constraint::Role::IsaType 0.53 + Specio::Constraint::Simple 0.53 + Specio::Constraint::Structurable 0.53 + Specio::Constraint::Structured 0.53 + Specio::Constraint::Union 0.53 + Specio::Declare 0.53 + Specio::DeclaredAt 0.53 + Specio::Exception 0.53 + Specio::Exporter 0.53 + Specio::Helpers 0.53 + Specio::Library::Builtins 0.53 + Specio::Library::Numeric 0.53 + Specio::Library::Perl 0.53 + Specio::Library::String 0.53 + Specio::Library::Structured 0.53 + Specio::Library::Structured::Dict 0.53 + Specio::Library::Structured::Map 0.53 + Specio::Library::Structured::Tuple 0.53 + Specio::OO 0.53 + Specio::PP 0.53 + Specio::PartialDump 0.53 + Specio::Registry 0.53 + Specio::Role::Inlinable 0.53 + Specio::Subs 0.53 + Specio::TypeChecks 0.53 + Specio::XS 0.53 + Test::Specio 0.53 requirements: B 0 Carp 0 Clone 0 + Clone::PP 0 Devel::StackTrace 0 Eval::Closure 0 Exporter 0 @@ -2266,6 +2366,7 @@ 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 @@ -2282,6 +2383,17 @@ DISTRIBUTIONS strict 0 version 0.83 warnings 0 + String-Print-0.96 + pathname: M/MA/MARKOV/String-Print-0.96.tar.gz + provides: + String::Print 0.96 + requirements: + Date::Parse 2.3 + Encode 0 + ExtUtils::MakeMaker 0 + HTML::Entities 0 + Test::More 0.86 + Unicode::GCString 0 Sub-Exporter-0.991 pathname: R/RJ/RJBS/Sub-Exporter-0.991.tar.gz provides: @@ -2314,11 +2426,11 @@ DISTRIBUTIONS perl 5.008000 strict 0 warnings 0 - Sub-Quote-2.006008 - pathname: H/HA/HAARG/Sub-Quote-2.006008.tar.gz + Sub-Quote-2.006009 + pathname: H/HA/HAARG/Sub-Quote-2.006009.tar.gz provides: - Sub::Defer 2.006008 - Sub::Quote 2.006008 + Sub::Defer 2.006009 + Sub::Quote 2.006009 requirements: ExtUtils::MakeMaker 0 Scalar::Util 0 @@ -2405,10 +2517,10 @@ DISTRIBUTIONS Test::Builder 0 Test::More 0.96 perl 5.012 - Test-Differences-0.71 - pathname: D/DC/DCANTRELL/Test-Differences-0.71.tar.gz + Test-Differences-0.72 + pathname: D/DC/DCANTRELL/Test-Differences-0.72.tar.gz provides: - Test::Differences 0.71 + Test::Differences 0.72 requirements: Capture::Tiny 0.24 Data::Dumper 2.126 @@ -2431,17 +2543,17 @@ DISTRIBUTIONS perl 5.006001 strict 0 warnings 0 - Test-Fatal-0.017 - pathname: R/RJ/RJBS/Test-Fatal-0.017.tar.gz + Test-Fatal-0.018 + pathname: R/RJ/RJBS/Test-Fatal-0.018.tar.gz provides: - Test::Fatal 0.017 + Test::Fatal 0.018 requirements: Carp 0 Exporter 5.57 ExtUtils::MakeMaker 6.78 Test::Builder 0 Try::Tiny 0.07 - strict 0 + perl 5.012 warnings 0 Test-Number-Delta-1.06 pathname: D/DA/DAGOLDEN/Test-Number-Delta-1.06.tar.gz @@ -2602,22 +2714,24 @@ DISTRIBUTIONS TimeDate 1.21 requirements: ExtUtils::MakeMaker 0 - Travel-Status-DE-DBRIS-0.11 - pathname: D/DE/DERF/Travel-Status-DE-DBRIS-0.11.tar.gz + Travel-Status-DE-DBRIS-0.18 + pathname: D/DE/DERF/Travel-Status-DE-DBRIS-0.18.tar.gz provides: - 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 + Travel::Status::DE::DBRIS 0.18 + Travel::Status::DE::DBRIS::Formation 0.18 + Travel::Status::DE::DBRIS::Formation::Carriage 0.18 + Travel::Status::DE::DBRIS::Formation::Group 0.18 + Travel::Status::DE::DBRIS::Formation::Sector 0.18 + Travel::Status::DE::DBRIS::Journey 0.18 + Travel::Status::DE::DBRIS::JourneyAtStop 0.18 + Travel::Status::DE::DBRIS::Location 0.18 + Travel::Status::DE::DBRIS::Operators 0.18 requirements: Carp 0 Class::Accessor 0.16 DateTime 0 DateTime::Format::Strptime 0 + File::Slurp 9999.19 Getopt::Long 0 JSON 0 LWP::Protocol::https 0 @@ -2628,18 +2742,18 @@ DISTRIBUTIONS Test::More 0 Test::Pod 0 perl v5.20.0 - 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 + Travel-Status-DE-HAFAS-6.23 + pathname: D/DE/DERF/Travel-Status-DE-HAFAS-6.23.tar.gz + provides: + Travel::Status::DE::HAFAS 6.23 + Travel::Status::DE::HAFAS::Journey 6.23 + Travel::Status::DE::HAFAS::Location 6.23 + Travel::Status::DE::HAFAS::Message 6.23 + Travel::Status::DE::HAFAS::Polyline 6.23 + Travel::Status::DE::HAFAS::Product 6.23 + Travel::Status::DE::HAFAS::Services 6.23 + Travel::Status::DE::HAFAS::Stop 6.23 + Travel::Status::DE::HAFAS::StopFinder 6.23 requirements: Carp 0 Class::Accessor 0.16 @@ -2657,12 +2771,12 @@ DISTRIBUTIONS Test::More 0 Test::Pod 0 perl v5.14.0 - Travel-Status-DE-IRIS-1.98 - pathname: D/DE/DERF/Travel-Status-DE-IRIS-1.98.tar.gz + Travel-Status-DE-IRIS-2.00 + pathname: D/DE/DERF/Travel-Status-DE-IRIS-2.00.tar.gz provides: - Travel::Status::DE::IRIS 1.98 - Travel::Status::DE::IRIS::Result 1.98 - Travel::Status::DE::IRIS::Stations 1.98 + Travel::Status::DE::IRIS 2.00 + Travel::Status::DE::IRIS::Result 2.00 + Travel::Status::DE::IRIS::Stations 2.00 requirements: Carp 0 Class::Accessor 0 @@ -2689,17 +2803,17 @@ DISTRIBUTIONS Text::LevenshteinXS 0 XML::LibXML 0 perl v5.14.2 - Travel-Status-DE-VRR-3.11 - pathname: D/DE/DERF/Travel-Status-DE-VRR-3.11.tar.gz - provides: - Travel::Status::DE::EFA 3.11 - Travel::Status::DE::EFA::Departure 3.11 - Travel::Status::DE::EFA::Info 3.11 - Travel::Status::DE::EFA::Line 3.11 - Travel::Status::DE::EFA::Services 3.11 - Travel::Status::DE::EFA::Stop 3.11 - Travel::Status::DE::EFA::Trip 3.11 - Travel::Status::DE::VRR 3.11 + Travel-Status-DE-VRR-3.17 + pathname: D/DE/DERF/Travel-Status-DE-VRR-3.17.tar.gz + provides: + Travel::Status::DE::EFA 3.17 + Travel::Status::DE::EFA::Departure 3.17 + Travel::Status::DE::EFA::Info 3.17 + Travel::Status::DE::EFA::Line 3.17 + Travel::Status::DE::EFA::Services 3.17 + Travel::Status::DE::EFA::Stop 3.17 + Travel::Status::DE::EFA::Trip 3.17 + Travel::Status::DE::VRR 3.17 requirements: Carp 0 Class::Accessor 0 @@ -2713,17 +2827,18 @@ DISTRIBUTIONS List::Util 0 Module::Build 0.4 Test::More 0 + URI::Escape 0 perl v5.10.1 - Travel-Status-MOTIS-0.01 - pathname: D/DE/DERF/Travel-Status-MOTIS-0.01.tar.gz + Travel-Status-MOTIS-0.03 + pathname: D/DE/DERF/Travel-Status-MOTIS-0.03.tar.gz provides: - Travel::Status::MOTIS 0.01 - Travel::Status::MOTIS::Polyline 0.01 - Travel::Status::MOTIS::Services 0.01 - Travel::Status::MOTIS::Stop 0.01 - Travel::Status::MOTIS::Stopover 0.01 - Travel::Status::MOTIS::Trip 0.01 - Travel::Status::MOTIS::TripAtStopover 0.01 + Travel::Status::MOTIS 0.03 + Travel::Status::MOTIS::Polyline 0.03 + Travel::Status::MOTIS::Services 0.03 + Travel::Status::MOTIS::Stop 0.03 + Travel::Status::MOTIS::Stopover 0.03 + Travel::Status::MOTIS::Trip 0.03 + Travel::Status::MOTIS::TripAtStopover 0.03 requirements: Carp 0 Class::Accessor 0.16 @@ -2762,63 +2877,66 @@ DISTRIBUTIONS requirements: ExtUtils::MakeMaker 0 common::sense 0 - 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 + URI-5.34 + pathname: O/OA/OALDERS/URI-5.34.tar.gz + provides: + URI 5.34 + URI::Escape 5.34 + URI::Heuristic 5.34 + URI::IRI 5.34 + URI::QueryParam 5.34 + URI::Split 5.34 + URI::URL 5.34 + URI::WithBase 5.34 + URI::data 5.34 + URI::file 5.34 + URI::file::Base 5.34 + URI::file::FAT 5.34 + URI::file::Mac 5.34 + URI::file::OS2 5.34 + URI::file::QNX 5.34 + URI::file::Unix 5.34 + URI::file::Win32 5.34 + URI::ftp 5.34 + URI::ftpes 5.34 + URI::ftps 5.34 + URI::geo 5.34 + URI::gopher 5.34 + URI::http 5.34 + URI::https 5.34 + URI::icap 5.34 + URI::icaps 5.34 + URI::irc 5.34 + URI::ircs 5.34 + URI::ldap 5.34 + URI::ldapi 5.34 + URI::ldaps 5.34 + URI::mailto 5.34 + URI::mms 5.34 + URI::news 5.34 + URI::nntp 5.34 + URI::nntps 5.34 + URI::otpauth 5.34 + URI::pop 5.34 + URI::rlogin 5.34 + URI::rsync 5.34 + URI::rtsp 5.34 + URI::rtspu 5.34 + URI::scp 5.34 + URI::sftp 5.34 + URI::sip 5.34 + URI::sips 5.34 + URI::smb 5.34 + URI::smtp 5.34 + URI::snews 5.34 + URI::ssh 5.34 + URI::telnet 5.34 + URI::tn3270 5.34 + URI::urn 5.34 + URI::urn::isbn 5.34 + URI::urn::oid 5.34 + URI::ws 5.34 + URI::wss 5.34 requirements: Carp 0 Cwd 0 @@ -2851,6 +2969,18 @@ DISTRIBUTIONS POSIX 0 Test::More 0 Time::HiRes 0 + Unicode-LineBreak-2019.001 + pathname: N/NE/NEZUMI/Unicode-LineBreak-2019.001.tar.gz + provides: + Text::LineFold 2018.012 + Unicode::GCString 2013.10 + Unicode::LineBreak 2019.001 + requirements: + Encode 1.98 + ExtUtils::MakeMaker 6.26 + MIME::Charset v1.6.2 + Test::More 0.45 + perl 5.008 Variable-Magic-0.64 pathname: V/VP/VPIT/Variable-Magic-0.64.tar.gz provides: @@ -3030,32 +3160,32 @@ DISTRIBUTIONS XSLoader 0 lib 0 perl 5.008001 - 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 + libwww-perl-6.81 + pathname: O/OA/OALDERS/libwww-perl-6.81.tar.gz + provides: + LWP 6.81 + LWP::Authen::Basic 6.81 + LWP::Authen::Digest 6.81 + LWP::Authen::Ntlm 6.81 + LWP::ConnCache 6.81 + LWP::Debug 6.81 + LWP::Debug::TraceHTTP 6.81 + LWP::DebugFile 6.81 + LWP::MemberMixin 6.81 + LWP::Protocol 6.81 + LWP::Protocol::cpan 6.81 + LWP::Protocol::data 6.81 + LWP::Protocol::file 6.81 + LWP::Protocol::ftp 6.81 + LWP::Protocol::gopher 6.81 + LWP::Protocol::http 6.81 + LWP::Protocol::loopback 6.81 + LWP::Protocol::mailto 6.81 + LWP::Protocol::nntp 6.81 + LWP::Protocol::nogo 6.81 + LWP::RobotUA 6.81 + LWP::Simple 6.81 + LWP::UserAgent 6.81 requirements: Digest::MD5 0 Encode 2.12 @@ -3069,10 +3199,11 @@ DISTRIBUTIONS HTML::HeadParser 3.71 HTTP::Cookies 6 HTTP::Date 6 + HTTP::Message 7.01 HTTP::Negotiate 6 - HTTP::Request 6.18 + HTTP::Request 7.01 HTTP::Request::Common 6.18 - HTTP::Response 6.18 + HTTP::Response 7.01 HTTP::Status 6.18 IO::Select 0 IO::Socket 0 diff --git a/doc/contributing.md b/doc/contributing.md new file mode 100644 index 0000000..a31ed4f --- /dev/null +++ b/doc/contributing.md @@ -0,0 +1,33 @@ +# Contributing to travelynx Development + +First, a note upfront: travelynx is a hobby project. +While I appreciate suggestions, bug reports, and merge requests / patches, I want to make sure that it remains a hobby project and does not turn into a chore. +As such, please do not expect a timely response to anything you submit. +I typically only address issues and merge requests when I have the capacity for them _and_ when doing so does not feel like a chore. + +That being said, I do appreciate bug reports, feature requests, and (simple!) patches, even if I may take quite a while to address or review them. +If you are planning a more involved patch set, please get in touch first. + +## Translations + +This is probably the easiest way to improve the life of any travelynx users who are not native German speakers. +Note that travelynx does _not_ use Weblate. + +### Updating or Extending Translations + +* Look at the [translation reference](../share/locales/reference.md) +* Pick a language that you'd like to fix / update / extend +* Adjust the corresponding `share/locales/ab-CD.po` file +* Open a merge request, either on [Codeberg](https://codeberg.org/derf/travelynx/pulls) or [GitHub](https://github.com/derf/travelynx/pulls) + +### Adding a new Language + +* Copy `share/locales/template.pot` to `share/locales/ab-CD.po`, replacing ab-CD with the appropriate language code +* Add the language / locale to `$self->helper(loc_handle …` in `lib/Travelynx.pm` +* Add the language / locale to `templates/language.html.ep` +* Provide as many translations as you feel comfortable with – partial translation files are fine; any entry left as `msgstr ""` will cause travelynx to fall back to English or German. +* Open a merge request, either on [Codeberg](https://codeberg.org/derf/travelynx/pulls) or [GitHub](https://github.com/derf/travelynx/pulls) + +## Bug Reports + +You may report bugs and request features either on [Codeberg](https://codeberg.org/derf/travelynx/issues) or [GitHub](https://github.com/derf/travelynx/issues). diff --git a/doc/setup.md b/doc/setup.md new file mode 100644 index 0000000..82a2348 --- /dev/null +++ b/doc/setup.md @@ -0,0 +1,136 @@ +# Hosting your own travelynx + +This document describes how to host your own travelynx instance. + +## Dependencies + + * perl ≥ 5.20 + * carton + * build-essential + * libpq-dev + * git + +## Installation + +travelynx depends on a set of Perl modules which are documented in `cpanfile`. +After installing the dependencies mentioned above, you can use carton to +install Perl depenencies locally. You may alsobe able to use cpanminus; +however this method is untested. + +In the project root directory (where `cpanfile` resides), run + +``` +carton install --deployment +``` + +and set `PERL5LIB=.../local/lib/perl5` before executing any travelynx +commands (see configs in the examples directory) or wrap them with `carton +exec`, e.g. `carton exec hypnotoad index.pl` + +## Setup + +First, you need to set up a PostgreSQL database so that travelynx can store +user accounts and journeys. It must be at least version 9.4 and must use a +UTF-8 locale. The following steps describe setup on a Debian 9 system; +setup on other distributions should be similar. + +* Write down a strong random password +* Create a postgres user for travelynx: `sudo -u postgres createuser -P travelynx` + (enter password when prompted) +* Create the database: `sudo -u postgres createdb -O travelynx travelynx` +* Copy `examples/travelynx.conf` to the application root directory + (the one in which `index.pl` resides) and edit it. Make sure to configure + db, cache, mail, and secrets. +* Initialize the database: `carton exec perl index.pl database migrate` + or `PERL5LIB=local/lib/perl5 perl index.pl database migrate` + +Your server also needs to be able to send mail. Set up your MTA of choice and +make sure that the sendmail binary can be used for outgoing mails. Mail +reception on the server is not required. + +Finally, configure the web service: + +* Set up a travelynx service using the service supervisor of your choice + (see `examples/travelynx.service` for a systemd unit file) +* Configure your web server to reverse-provy requests to the travelynx + instance. See `examples/nginx-site` for an nginx config. +* Install a `timeout 5m perl index.pl work -m production` cronjob. It is used + to update realtime data and perform automatic checkout and should run + every three minutes or so, see `examples/cron`. + +You can now start the travelynx service, navigate to the website and register +your first account. There is no admin account, all management is performed +via cron or (in non-standard cases) on the command line. + +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 + +It is recommended to run travelynx directly from the git repository. When +updating, the workflow depends on whether schema updates need to be applied +or not. + +``` +git pull +carton install --deployment # if you are using carton: update dependencies +chmod -R a+rX . # only needed if travelynx is running under a different user +if perl index.pl database has-current-schema; then + systemctl reload travelynx +else + systemctl stop travelynx + perl index.pl database migrate + systemctl start travelynx +fi +``` + +Note that this is subject to change -- the application may perform schema +updates automatically in the future. If you used carton for installation, +use `carton exec perl ...` in the snippet above; otherwise, export +`PERL5LIB=.../local/lib/perl5`. + +## Setup with Docker +--- + +Note that travelynx Docker support is experimental and, in its current form, +far from best practices. Pull requests are appreciated. + +First, you need to set up a PostgreSQL database so that travelynx can store +user accounts and journeys. It must be at least version 9.4 and must use a +UTF-8 locale. See above (or `examples/docker/postgres-init.sh`) for database +initialization. You do not need to perform the `database migrate` step. + +Next, you need to prepare three files that will be mounted into the travelynx +container: travelynx configuration, e-mail configuration, and imprint and +privacy policy. For the sake of this readme, we assume that you are using the +`local/` directory to store these + +* `mkdir local` +* copy examples/travelynx.conf to local/travelynx.conf and configure it. +* copy examples/docker/email-transport.sh to local/email-transport.sh and configure it. + 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. + +travelynx consists of two runtimes: the web application and a background +worker. Your service supervisor (or docker compose / docker stack / kubernetes +setup) should orchestrate them somewhere along these lines. + +* `docker pull derfnull/travelynx:latest` +* Start web application: `docker run -p 8093:8093 -v ${PWD}/local:/local:ro travelynx:latest` +* Wait until localhost:8093 responds to requests +* Start worker: `docker run -v ${PWD}/local:/local:ro travelynx:latest worker` + +To install an update: stop worker and web application, update the travelynx +image, and start them again. Database migrations will be performed +automatically. Note that downgrades are not supported. diff --git a/doc/usage.md b/doc/usage.md new file mode 100644 index 0000000..2d8bb42 --- /dev/null +++ b/doc/usage.md @@ -0,0 +1,34 @@ +# travelynx primer + +For the sake of this manual, we will assume your travelynx instance is running +on `travelynx.de` + +travelynx journey logging is based on checkin and checkout actions: You check +into a train when boarding it, select a destination, and are automatically +checked out when you arrive. Real-time data is saved on both occasions and +continuously updated while in transit, providing an accurate overview of both +scheduled and actual journey times. + +## Checking in + +You can check into a train at nearly any point in time, though it's usually a +good idea to do it within a 30-minute window befor/after its departure. The +precise constraints depend on the selected backend (i.e., data provider). + +First, you need to select the stop you want to check in from. Navigate to +`travelynx.de` or click/tap on the travelynx text in the navigation bar. You +will see a list of the five stops closest to your current location (as reported +by your browser). Select the stop you're at or enter its name manually. + +As soon as you select a train, you will be checked in and travelynx will switch +to the journey / checkout view. If you already know where you're headed, you +should click/tap on the destination stop in the stop list now. You can change +the destination by selecting a new one anytime. + +## Checking out + +You are automatically checked out a few minutes after arrival at your +destination. If the train has already arrived when you select a destination and +its arrival was less than two hours ago, you are checked out immediately. If +it's more than two hours, you need to perform a manual checkout (without +arrival data) using the link at the bottom of the checkin menu's stop list. diff --git a/ext/dbdb b/ext/dbdb new file mode 160000 +Subproject eacf5450dd79def235232e452ee8d9a1f700af9 diff --git a/lib/Travelynx.pm b/lib/Travelynx.pm index b36c633..a0661bf 100755 --- a/lib/Travelynx.pm +++ b/lib/Travelynx.pm @@ -21,11 +21,11 @@ use List::Util; use List::UtilsBy qw(uniq_by); use List::MoreUtils qw(first_index); 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::Locales; use Travelynx::Helper::MOTIS; use Travelynx::Helper::Sendmail; use Travelynx::Helper::Traewelling; @@ -68,6 +68,8 @@ sub startup { $self->types->type( csv => 'text/csv; charset=utf-8' ); $self->types->type( json => 'application/json; charset=utf-8' ); + $self->types->type( gpx => 'application/gpx+xml; charset=utf-8' ); + $self->types->type( xml => 'text/xml; charset=utf-8' ); $self->plugin('Config'); @@ -157,6 +159,14 @@ sub startup { } ); + $self->hook( + 'before_render' => sub { + my ($self) = @_; + + $self->stash( loc_handle => $self->loc_handle ); + } + ); + $self->attr( cache_iris_main => sub { my ($self) = @_; @@ -242,7 +252,8 @@ sub startup { state $dbris = Travelynx::Helper::DBRIS->new( log => $self->app->log, service_config => $self->app->config->{dbris}, - cache => $self->app->cache_iris_rt, + realtime_cache => $self->app->cache_iris_rt, + main_cache => $self->app->cache_iris_main, root_url => $self->base_url_for('/')->to_abs, user_agent => $self->ua, version => $self->app->config->{version}, @@ -287,6 +298,7 @@ sub startup { user_agent => $self->ua, root_url => $self->base_url_for('/')->to_abs, version => $self->app->config->{version}, + time_zone => 'Europe/Berlin', ); } ); @@ -397,16 +409,59 @@ sub startup { ); $self->helper( - dbdb => sub { + loc_handle => sub { my ($self) = @_; - state $dbdb = Travelynx::Helper::DBDB->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}, - ); + + my @languages; + if ( $self->is_user_authenticated + and @{ $self->current_user->{languages} } ) + { + @languages = @{ $self->current_user->{languages} }; + } + elsif ( my $languages = $self->req->headers->accept_language ) { + for my $lang ( split( qr{ \s* , \s* }x, $languages ) ) { + if ( $lang =~ m{ ^ de }x ) { + push( @languages, 'de-DE' ); + } + elsif ( $lang =~ m{ ^ en }x ) { + push( @languages, 'en-GB' ); + } + elsif ( $lang =~ m{ ^ fr }x ) { + push( @languages, 'fr-FR' ); + } + elsif ( $lang =~ m{ ^ hu }x ) { + push( @languages, 'hu-HU' ); + } + elsif ( $lang =~ m{ ^ pl }x ) { + push( @languages, 'pl-PL' ); + } + } + } + + # en-GB and de-DE serve as fall-back languages, both in case + # we do not have the handle we need (here) and in case a string + # has only been translated to some languages (below). + + my $handle + = Travelynx::Helper::Locales->get_handle( @languages, 'en-GB', + 'de-DE' ); + my $first_fallback + = Travelynx::Helper::Locales->get_handle('en-GB'); + my $second_fallback + = Travelynx::Helper::Locales->get_handle('de-DE'); + + $handle->fail_with( + sub { $first_fallback->maketext( @_[ 1 .. $#_ ] ) } ); + $first_fallback->fail_with( + sub { $second_fallback->maketext( @_[ 1 .. $#_ ] ) } ); + return $handle; + } + ); + + $self->helper( + 'L' => sub { + my ( $self, @args ) = @_; + $self->stash('loc_handle')->maketext(@args); } ); @@ -1160,7 +1215,7 @@ 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 ) { + if ( $opt{hafas} eq 'ÖBB' and $journey->class <= 16 ) { $self->add_wagonorder( uid => $uid, train_id => $journey->id, @@ -1315,7 +1370,8 @@ sub startup { if ( $user->{is_dbris} or $user->{is_efa} or $user->{is_hafas} - or $user->{is_motis} ) + or $user->{is_motis} + or $train_id eq 'manual' ) { return $self->_checkout_journey_p(%opt); } @@ -1446,7 +1502,9 @@ sub startup { if ($has_arrived) { my @unknown_stations = $self->stations->grep_unknown( - $train->route ); + backend_id => $user->{backend_id}, + names => [ $train->route ] + ); if (@unknown_stations) { $self->app->log->warn( sprintf( @@ -1613,6 +1671,13 @@ sub startup { rt_arrival => ( $stop->[2]{rt_arr} || $stop->[2]{sched_arr} ) ); + if ( $stop->[2]{platform} ) { + $self->in_transit->set_arrival_platform( + uid => $uid, + db => $db, + arrival_platform => $stop->[2]{platform} + ); + } if ( $now > ( $stop->[2]{rt_arr} || $stop->[2]{sched_arr} ) ) { @@ -1806,38 +1871,25 @@ sub startup { 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( + $self->dbris->get_wagonorder_p(%opt)->then( sub { - my ($wagonorder) = @_; + my ($status) = @_; + my $wr = $status->result; 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} ) + and $wr ) { my $dt = $opt{datetime}->clone->set_time_zone('UTC'); - $data->{wagonorder_dep} = $wagonorder; + $data->{wagonorder_dep} = $status->{raw_json}; $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}, + tt => $opt{train_type}, + tn => $opt{train_no}, + dt => $dt->epoch, + eva => $opt{eva} }; $user_data->{wagongroups} = []; for my $group ( $wr->groups ) { @@ -1884,10 +1936,8 @@ sub startup { train_id => $train_id, ); } - elsif ( $opt{is_arrival} - and not exists $wagonorder->{error} ) - { - $data->{wagonorder_arr} = $wagonorder; + elsif ( $opt{is_arrival} ) { + $data->{wagonorder_arr} = $status->{raw_json}; $self->in_transit->update_data( uid => $uid, db => $db, @@ -1899,6 +1949,10 @@ sub startup { } )->catch( sub { + my ($err) = @_; + $self->log->debug( + "add_wagonorder: promise rejected with ${err}"); + # no wagonorder? no problem. return; } @@ -2086,47 +2140,36 @@ sub startup { my $db = $self->pg->db; if ($is_departure) { - $self->dbdb->get_stationinfo_p($dep_eva)->then( - sub { - my ($station_info) = @_; - my $data = { stationinfo_dep => $station_info }; + if ( my $si + = $self->stations->get_bahn_stationinfo( eva => $dep_eva ) ) + { + my $data = { stationinfo_dep => $si }; - $self->in_transit->update_data( - uid => $uid, - db => $db, - data => $data, - train_id => $train_id, - ); - return; - } - )->catch( - sub { - # no stationinfo? no problem. - return; - } - )->wait; + $self->in_transit->update_data( + uid => $uid, + db => $db, + data => $data, + train_id => $train_id, + ); + return; + } } 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 }; + if ( my $si + = $self->stations->get_bahn_stationinfo( eva => $arr_eva ) ) + { - $self->in_transit->update_data( - uid => $uid, - db => $db, - data => $data, - train_id => $train_id, - ); - return; - } - )->catch( - sub { - # no stationinfo? no problem. - return; - } - )->wait; + my $data = { stationinfo_arr => $si }; + + $self->in_transit->update_data( + uid => $uid, + db => $db, + data => $data, + train_id => $train_id, + ); + return; + } } } ); @@ -2402,6 +2445,7 @@ sub startup { backend_id => $latest->{backend_id}, backend_name => $latest->{backend_name}, is_dbris => $latest->{is_dbris}, + is_efa => $latest->{is_efa}, is_iris => $latest->{is_iris}, is_hafas => $latest->{is_hafas}, is_motis => $latest->{is_motis}, @@ -2468,6 +2512,7 @@ sub startup { backend => { id => $status->{backend_id}, type => $status->{is_dbris} ? 'DBRIS' + : $status->{is_efa} ? 'EFA' : $status->{is_hafas} ? 'HAFAS' : $status->{is_motis} ? 'MOTIS' : 'IRIS-TTS', @@ -2695,14 +2740,46 @@ sub startup { latlon => $_->{from_latlon} // $_->{dep_latlon} } } @journeys; + my @extra_stations; + + if ( $opt{show_all_stops} ) { + for my $journey (@journeys) { + my @j_stops = map { + { + name => $_->[2], + latlon => [ $_->[1], $_->[0] ] + } + } grep { defined $_->[2] } + @{ $journey->{polyline} // [] }; + @extra_stations + = uniq_by { $_->{name} } ( @extra_stations, @j_stops ); + } + } my @station_coordinates = map { [ $_->{latlon}, $_->{name} ] } @stations; + my @extra_station_coordinates + = map { [ $_->{latlon}, $_->{name} ] } @extra_stations; + + my @now_coordinates; + if ( $opt{with_now_markers} ) { + @now_coordinates = map { + [ + $_->{now_latlon}, + $_->{train_type} . ' ' + . ( $_->{train_line} // $_->{train_no} ) + ] + } @journeys; + } my @station_pairs; my @polylines; my %seen; + # not part of the travelled route, but trip route before/after the journey. + # Only used if show_full_route is set. + my @extra_polylines; + my @skipped_journeys; my @polyline_journeys = grep { $_->{polyline} } @journeys; my @beeline_journeys = grep { not $_->{polyline} } @journeys; @@ -2720,10 +2797,14 @@ sub startup { 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; + # poly_dep_index, poly_arr_index are only available for + # journeys that were processed by get_travel_distance + # beforehand. However, they are much less error-prone than this + # first_index / last_index kludge when it comes to ring lines. + my $from_index = $journey->{poly_dep_index} + // first_index { $_->[2] and $_->[2] == $from_eva } @polyline; + my $to_index = $journey->{poly_arr_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 ) { @@ -2778,12 +2859,28 @@ sub startup { if ( $from_index > $to_index ) { ( $to_index, $from_index ) = ( $from_index, $to_index ); } + if ( $opt{show_full_route} ) { + my @pre_polyline = @polyline[ 0 .. $from_index ]; + my @post_polyline = @polyline[ $to_index .. $#polyline ]; + my @pre_polyline_coords; + for my $coord (@pre_polyline) { + push( @pre_polyline_coords, + [ $coord->[1], $coord->[0] ] ); + } + my @post_polyline_coords; + for my $coord (@post_polyline) { + push( @post_polyline_coords, + [ $coord->[1], $coord->[0] ] ); + } + push( @extra_polylines, + ( \@pre_polyline_coords, \@post_polyline_coords ) ); + } @polyline = @polyline[ $from_index .. $to_index ]; my @polyline_coords; for my $coord (@polyline) { push( @polyline_coords, [ $coord->[1], $coord->[0] ] ); } - push( @polylines, [@polyline_coords] ); + push( @polylines, \@polyline_coords ); } for my $journey (@beeline_journeys) { @@ -2891,7 +2988,7 @@ sub startup { { polylines => $json->encode( \@station_pairs ), color => '#673ab7', - opacity => @polylines + opacity => scalar @polylines ? $with_polyline ? 0.4 : 0.6 @@ -2901,8 +2998,14 @@ sub startup { polylines => $json->encode( \@polylines ), color => '#673ab7', opacity => 0.8, - } + }, + { + polylines => $json->encode( \@extra_polylines ), + color => '#665577', + opacity => 0.6, + }, ], + markers => \@now_coordinates, }; if (@station_coordinates) { @@ -2916,6 +3019,13 @@ sub startup { = [ [ $min_lat, $min_lon ], [ $max_lat, $max_lon ] ]; } + if (@extra_station_coordinates) { + $ret->{station_coordinates} = [ + uniq_by { $_->[1] } + ( @station_coordinates, @extra_station_coordinates ) + ]; + } + return $ret; } ); @@ -2983,10 +3093,17 @@ sub startup { if ( $self->is_user_authenticated ) { return 1; } - $self->render( - 'login', - redirect_to => $self->req->url, - from => 'auth_required' + $self->respond_to( + json => { + json => { error => 'authentication required' }, + status => 401 + }, + any => { + template => 'login', + status => 401, + redirect_to => $self->req->url, + from => 'auth_required' + } ); return undef; } @@ -3000,9 +3117,11 @@ 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/language')->to('account#change_language'); $authed_r->get('/ajax/status_card.html')->to('traveling#status_card'); $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'); @@ -3017,10 +3136,19 @@ sub startup { $authed_r->get('/history/:year')->to('traveling#yearly_history'); $authed_r->get('/history/:year/review')->to('traveling#year_in_review'); $authed_r->get('/history/:year/:month')->to('traveling#monthly_history'); - $authed_r->get('/journey/add')->to('traveling#add_journey_form'); + $authed_r->get('/journey/add') + ->to('traveling#add_journey_form') + ->name('add_journey'); $authed_r->get('/journey/comment')->to('traveling#comment_form'); $authed_r->get('/journey/visibility')->to('traveling#visibility_form'); - $authed_r->get('/journey/:id')->to('traveling#journey_details'); + $authed_r->get( '/journey/:id' => [ format => [ 'html', 'json' ] ] ) + ->to( 'traveling#journey_details', format => undef ) + ->name('journey'); + $authed_r->get( '/polyline/:id' => [ format => [ 'gpx', 'json' ] ] )->to( + 'traveling#journey_details', + format => undef, + polyline_export => 1 + )->name('polyline_download'); $authed_r->get('/s/*station')->to('traveling#station'); $authed_r->get('/confirm_mail/:token')->to('account#confirm_mail'); $authed_r->post('/account/privacy')->to('account#privacy'); @@ -3029,8 +3157,11 @@ 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/language')->to('account#change_language'); $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('/polyline/set')->to('traveling#set_polyline'); $authed_r->post('/journey/comment')->to('traveling#comment_form'); $authed_r->post('/journey/visibility')->to('traveling#visibility_form'); $authed_r->post('/journey/edit')->to('traveling#edit_journey'); diff --git a/lib/Travelynx/Command/database.pm b/lib/Travelynx/Command/database.pm index 675f0a7..5792e5f 100644 --- a/lib/Travelynx/Command/database.pm +++ b/lib/Travelynx/Command/database.pm @@ -7,7 +7,7 @@ package Travelynx::Command::database; use Mojo::Base 'Mojolicious::Command'; use DateTime; -use File::Slurp qw(read_file); +use File::Slurp qw(read_dir read_file); use List::Util qw(); use JSON; use Travel::Status::DE::EFA; @@ -3184,8 +3184,446 @@ qq{select distinct checkout_station_id from in_transit where backend_id = 0;} } ); }, + + # 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; + } + ); + }, + + # v65 -> v66 + # Relax platform and line length constraints for EFA APIs (and possibly MOTIS) + 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_line type varchar(64); + alter table in_transit alter column arr_platform type varchar(64); + alter table in_transit alter column dep_platform type varchar(64); + alter table journeys alter column train_line type varchar(64); + alter table journeys alter column arr_platform type varchar(64); + alter table journeys alter column dep_platform type varchar(64); + + 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 = 66; + } + ); + }, + + # v66 -> v67 + # Add language settings to profile + sub { + my ($db) = @_; + $db->query( + qq{ + drop view users_with_backend; + alter table users add column language varchar(128); + update schema_version set version = 67; + create view users_with_backend as select + users.id as id, users.name as name, status, public_level, + language, 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 + ; + } + ); + }, + + # v67 -> v68 + # Of course there are backends with stop names that are >64 chars long + sub { + my ($db) = @_; + $db->query( + qq{ + drop view stations_str; + drop view stations_with_external_ids; + drop view in_transit_str; + drop view journeys_str; + drop view follows_in_transit; + alter table stations alter column name type varchar(128); + 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; + 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 + ; + 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 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 = 68; + } + ); + }, + + # v68 -> v69 + # Incorporate dbdb (entry/exit direction) data into travelynx + # This avoids having to make web requests to lib.finalrewind.org/dbdb, + # and allows for also showing the exit direction for intermediate stops. + sub { + my ($db) = @_; + $db->query( + qq{ + alter table schema_version + add column dbdb varchar(12); + create table bahn_platform_directions ( + eva integer primary key, + data jsonb not null + ); + } + ); + sync_dbdb($db); + $db->query( + qq{ + update schema_version set version = 69; + update schema_version set dbdb = '2025-10-27'; + } + ); + }, ); +sub sync_dbdb { + my ($db) = @_; + + my $json = JSON->new; + + for my $file ( read_dir( 'ext/dbdb/s', prefix => 1 ) ) { + if ( $file !~ m{\.txt$} ) { + next; + } + + my %station; + for my $line ( read_file( $file, { binmode => ':encoding(utf-8)' } ) ) { + if ( $line + =~ m{ ^ \s* (?<platform> \d+ ) \s+ (?<type> \S+ ) \s+ (?<direction> \S+ ) }x + ) + { + $station{ $+{platform} } = { + kopfgleis => $+{type} eq 'K' ? \1 : \0, + direction => $+{direction}, + }; + } + elsif ( $line + =~ m{ ^ @ \s* (?<stations> [^:]+ ) : \s* (?<platforms> .+ ) $ }x + ) + { + my $stations_raw = $+{stations}; + my $platforms_raw = $+{platforms}; + my @stations = split( qr{, }, $stations_raw ); + my @platforms = split( qr{, }, $platforms_raw ); + for my $platform (@platforms) { + my ( $number, $direction ) = split( qr{ }, $platform ); + for my $from_station (@stations) { + $station{$number}{direction_from}{$from_station} + = $direction; + } + } + } + } + my ($station_name) = ( $file =~ m{ s / ([^.]*) . txt $ }x ); + my ($station) + = Travel::Status::DE::IRIS::Stations::get_station($station_name); + if ( $station and $station->[0] eq $station_name ) { + $db->insert( + 'bahn_platform_directions', + { + eva => $station->[2], + data => $json->encode( \%station ) + }, + { on_conflict => \'(eva) do update set data = EXCLUDED.data' } + ); + } + elsif ( not $station ) { + say STDERR "DBDB import: unknown station: $station_name"; + } + else { + say STDERR +"DBDB import: station mismatch: wanted to import $station_name, but got " + . $station->[0]; + } + } +} + sub sync_stations { my ( $db, $iris_version ) = @_; diff --git a/lib/Travelynx/Command/dumpstops.pm b/lib/Travelynx/Command/dumpstops.pm index 4d20bbd..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 @@ -24,13 +24,13 @@ sub run { or die("open($filename): $!\n"); my $csv = Text::CSV->new( { eol => "\r\n" } ); - $csv->combine(qw(name eva lat lon backend is_iris is_hafas)); + $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 backend is_iris is_hafas}} ); + @{$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/integritycheck.pm b/lib/Travelynx/Command/integritycheck.pm index be5fe71..907d484 100644 --- a/lib/Travelynx/Command/integritycheck.pm +++ b/lib/Travelynx/Command/integritycheck.pm @@ -76,7 +76,8 @@ sub run { my %notified; my $rename = $self->app->renamed_station; - my $res = $db->select( 'journeys', [ 'route', 'edited' ] )->expand; + my $res = $db->select( 'journeys', [ 'backend_id', 'route', 'edited' ] ) + ->expand; while ( my $j = $res->hash ) { if ( $j->{edited} & 0x0010 ) { @@ -89,8 +90,10 @@ sub run { $stop->[0] = $rename->{ $stop->[0] }; } } - my @unknown - = $self->app->stations->grep_unknown( map { $_->[0] } @stops ); + my @unknown = $self->app->stations->grep_unknown( + backend_id => $j->{backend_id}, + names => [ map { $_->[0] } @stops ] + ); for my $stop_name (@unknown) { if ( not $notified{$stop_name} ) { if ( not $found ) { diff --git a/lib/Travelynx/Command/maintenance.pm b/lib/Travelynx/Command/maintenance.pm index 7baf762..7a8ae16 100644 --- a/lib/Travelynx/Command/maintenance.pm +++ b/lib/Travelynx/Command/maintenance.pm @@ -121,7 +121,7 @@ sub run { push( @uids_to_delete, $to_delete->arrays->map( sub { shift->[0] } )->each ); - if ( @uids_to_delete > 10 ) { + if ( @uids_to_delete > 60 ) { printf STDERR ( "About to delete %d accounts, which is quite a lot.\n", scalar @uids_to_delete diff --git a/lib/Travelynx/Command/stats.pm b/lib/Travelynx/Command/stats.pm new file mode 100644 index 0000000..953c75d --- /dev/null +++ b/lib/Travelynx/Command/stats.pm @@ -0,0 +1,59 @@ +package Travelynx::Command::stats; + +# Copyright (C) 2020-2023 Birte Kristina Friesel +# +# SPDX-License-Identifier: AGPL-3.0-or-later +use Mojo::Base 'Mojolicious::Command'; + +use DateTime; + +has description => 'Deal with monthly and yearly statistics'; + +has usage => sub { shift->extract_usage }; + +sub refresh_all { + my ($self) = @_; + + my $db = $self->app->pg->db; + my $now = DateTime->now( time_zone => 'Europe/Berlin' ); + + say 'Refreshing all stats, this may take a while ...'; + + my $total = $db->select( 'users', 'count(*) as count', { status => 1 } ) + ->hash->{count}; + my $i = 1; + + for + my $user ( $db->select( 'users', ['id'], { status => 1 } )->hashes->each ) + { + $self->app->journeys->generate_missing_stats( uid => $user->{id} ); + $self->app->journeys->get_stats( + uid => $user->{id}, + year => $now->year, + write_only => 1, + ); + if ( $i == $total or ( $i % 10 ) == 0 ) { + printf( "%.f%% complete\n", $i * 100 / $total ); + } + $i++; + } +} + +sub run { + my ( $self, $cmd, @arg ) = @_; + + if ( $cmd eq 'refresh-all' ) { + $self->refresh_all(@arg); + } + +} + +1; + +__END__ + +=head1 SYNOPSIS + + Usage: index.pl stats refresh-all + + Refreshes all stats diff --git a/lib/Travelynx/Command/translation.pm b/lib/Travelynx/Command/translation.pm new file mode 100644 index 0000000..cc3a5ac --- /dev/null +++ b/lib/Travelynx/Command/translation.pm @@ -0,0 +1,99 @@ +package Travelynx::Command::translation; + +# Copyright (C) 2025 Birte Kristina Friesel +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +use Mojo::Base 'Mojolicious::Command'; +use Travelynx::Helper::Locales; + +has description => 'Export translation status'; + +has usage => sub { shift->extract_usage }; + +sub run { + my ( $self, $command ) = @_; + + my @locales = (qw(de-DE en-GB fr-FR hu-HU pl-PL)); + + my %count; + my %handle; + for my $locale (@locales) { + $handle{$locale} = Travelynx::Helper::Locales->get_handle($locale); + $handle{$locale}->fail_with('failure_handler_auto'); + $count{$locale} = 0; + } + + binmode( STDOUT, ':encoding(utf-8)' ); + + if ( not $command ) { + $self->help; + } + elsif ( $command eq 'update-ref' ) { + my @buf; + + open( my $fh, '<:encoding(utf-8)', 'share/locales/de_DE.po' ); + my $comment; + for my $line (<$fh>) { + chomp $line; + if ( $line =~ m{ ^ [#] \s+ (.*) $ }x ) { + push( @buf, "## $1\n" ); + } + elsif ( $line =~ m{ ^ [#] , \s+ (.*) $ }x ) { + $comment = $1; + } + elsif ( $line =~ m{ ^ msgid \s+ " (.*) " $ }x ) { + my $id = $1; + push( @buf, "### ${id}\n" ); + if ($comment) { + push( @buf, '*' . $comment . "*\n" ); + $comment = undef; + } + for my $locale (@locales) { + my $translation = $handle{$locale}->maketext($id); + if ( $translation ne $id ) { + push( @buf, "* ${locale}: ${translation}" ); + $count{$locale} += 1; + } + else { + push( @buf, "* ${locale} *missing*" ); + } + } + push( @buf, q{} ); + } + } + close($fh); + + open( $fh, '>:encoding(utf-8)', 'share/locales/reference.md' ); + say $fh '# Translation Status'; + say $fh q{}; + for my $locale (@locales) { + say $fh sprintf( + '* %s: %.1f%% complete (%d missing)', + $locale, + $count{$locale} * 100 / $count{'de-DE'}, + $count{'de-DE'} - $count{$locale}, + ); + } + say $fh q{}; + for my $line (@buf) { + say $fh $line; + } + close($fh); + } + else { + $self->help; + } +} + +1; + +__END__ + +=head1 SYNOPSIS + + Usage: index.pl translation <command> + + Supported commands: + + * update-ref: update share/locales/reference.md diff --git a/lib/Travelynx/Command/work.pm b/lib/Travelynx/Command/work.pm index 60417b1..dc58a48 100644 --- a/lib/Travelynx/Command/work.pm +++ b/lib/Travelynx/Command/work.pm @@ -18,7 +18,7 @@ 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 ); @@ -53,16 +53,34 @@ sub run { my $arr = $entry->{arr_eva}; my $train_id = $entry->{train_id}; - if ( $entry->{is_dbris} ) { + if ( $train_id eq 'manual' ) { + if ( $arr + and $entry->{real_arr_ts} + and $now->epoch - $entry->{real_arr_ts} > 900 ) + { + $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' ) ) + { eval { - Mojo::Promise->timer( $dbris_rate_limited ? 4.5 : 1.0 )->then( + Mojo::Promise->timer( + $dbris_rate_limited ? 4.5 : ( $backend ? 2.0 : 1.0 ) ) + ->then( sub { return $self->app->dbris->get_journey_p( trip_id => $train_id ); } - )->then( + )->then( sub { my ($journey) = @_; @@ -155,7 +173,7 @@ sub run { )->wait; } } - )->catch( + )->catch( sub { my ($err) = @_; $self->app->log->debug( @@ -169,11 +187,11 @@ sub run { $backend_issues += 1; } } - )->wait; + )->wait; if ( $arr and $entry->{real_arr_ts} - and $now->epoch - $entry->{real_arr_ts} > 600 ) + and $now->epoch - $entry->{real_arr_ts} > 900 ) { $self->app->checkout_p( station => $arr, @@ -189,10 +207,9 @@ sub run { $self->app->log->error( "work($uid) @ DBRIS $entry->{backend_name}: $@"); } - next; } - if ( $entry->{is_efa} ) { + elsif ( $entry->{is_efa} and ( not $backend or $backend eq 'efa' ) ) { eval { $self->app->efa->get_journey_p( trip_id => $train_id, @@ -269,7 +286,7 @@ sub run { if ( $arr and $entry->{real_arr_ts} - and $now->epoch - $entry->{real_arr_ts} > 600 ) + and $now->epoch - $entry->{real_arr_ts} > 900 ) { $self->app->checkout_p( station => $arr, @@ -285,10 +302,10 @@ sub run { $self->app->log->error( "work($uid) @ EFA $entry->{backend_name}: $@"); } - next; } - if ( $entry->{is_motis} ) { + elsif ( $entry->{is_motis} and ( not $backend or $backend eq 'motis' ) ) + { eval { $self->app->motis->get_trip_p( @@ -309,6 +326,10 @@ sub run { stop => $stopover->stop, motis => $entry->{backend_name}, ); + + $self->app->log->debug( "mapped " + . $stopover->stop->id . " to " + . $stopover->stop->{eva} ); } } @@ -358,7 +379,7 @@ sub run { )->catch( sub { my ($err) = @_; - $self->app->log->error( + $self->app->log->debug( "work($uid) @ MOTIS $entry->{backend_name}: journey: $err" ); } @@ -366,7 +387,7 @@ sub run { if ( $arr and $entry->{real_arr_ts} - and $now->epoch - $entry->{real_arr_ts} > 600 ) + and $now->epoch - $entry->{real_arr_ts} > 900 ) { $self->app->checkout_p( station => $arr, @@ -382,10 +403,10 @@ sub run { $self->app->log->error( "work($uid) @ MOTIS $entry->{backend_name}: $@"); } - next; } - if ( $entry->{is_hafas} ) { + elsif ( $entry->{is_hafas} and ( not $backend or $backend eq 'hafas' ) ) + { eval { @@ -437,8 +458,9 @@ sub run { is_departure => 1, eva => $dep, datetime => $found_dep->sched_dep, - train_type => $journey->type =~ s{ +$}{}r, - train_no => $journey->number, + train_type => ( $journey->type // q{} ) + =~ s{ +$}{}r, + train_no => $journey->number, ); $self->app->add_stationinfo( $uid, 1, $journey->id, $found_dep->loc->eva ); @@ -500,7 +522,7 @@ sub run { if ( $arr and $entry->{real_arr_ts} - and $now->epoch - $entry->{real_arr_ts} > 600 ) + and $now->epoch - $entry->{real_arr_ts} > 900 ) { $self->app->checkout_p( station => $arr, @@ -516,7 +538,6 @@ sub run { $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. @@ -528,182 +549,186 @@ sub run { # 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 ); - $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 ); - } + }; + if ($@) { + $errors += 1; + $self->app->log->error("work($uid) @ IRIS: departure: $@"); } - }; - if ($@) { - $errors += 1; - $self->app->log->error("work($uid) @ IRIS: 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 - ); - $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 => 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; + }; + if ($@) { + $self->app->log->error("work($uid) @ IRIS: arrival: $@"); + $errors += 1; } - }; - if ($@) { - $self->app->log->error("work($uid) @ IRIS: arrival: $@"); - $errors += 1; + + eval { }; } - eval { }; } my $started_at = $now; @@ -711,15 +736,19 @@ 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},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}" + . " 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},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}" +"worker${tags} runtime_seconds=${worker_duration},errors=${errors},backend_errors=${backend_issues},ratelimit_count=${rate_limit_counts}" )->wait; } } diff --git a/lib/Travelynx/Controller/Account.pm b/lib/Travelynx/Controller/Account.pm index 0978c88..b0722f7 100644 --- a/lib/Travelynx/Controller/Account.pm +++ b/lib/Travelynx/Controller/Account.pm @@ -345,9 +345,9 @@ sub register { } if ( not $dt - or DateTime->now( time_zone => 'Europe/Berlin' )->epoch - $dt < 6 ) + or DateTime->now( time_zone => 'Europe/Berlin' )->epoch - $dt < 10 ) { - # a human user should take at least five seconds to fill out the form. + # a human user should take at least ten seconds to fill out the form. # Throw a CSRF error at presumed spammers. $self->render( 'bad_request', @@ -874,6 +874,35 @@ sub webhook { $self->render( 'webhooks', hook => $hook ); } +sub change_language { + my ($self) = @_; + + my $action = $self->req->param('action'); + my $language = $self->req->param('language'); + + if ( $action and $action eq 'save' ) { + if ( $self->validation->csrf_protect->has_error('csrf_token') ) { + $self->render( + 'bad_request', + csrf => 1, + status => 400 + ); + return; + } + $self->users->set_language( + uid => $self->current_user->{id}, + language => $language eq 'none' ? undef : $language, + ); + $self->flash( success => 'language' ); + $self->redirect_to('account'); + } + else { + my @languages = @{ $self->current_user->{languages} }; + $self->param( language => $languages[0] // 'none' ); + $self->render('language'); + } +} + sub change_mail { my ($self) = @_; @@ -1026,6 +1055,7 @@ sub backend_form { my ($self) = @_; my $user = $self->current_user; + my %backend_by_id; my @backends = $self->stations->get_backends; my @suggested_backends; @@ -1084,12 +1114,13 @@ sub backend_form { $backend->{homepage} = $s->{homepage}; $backend->{regions} = [ map { $place_map{$_} // $_ } @{ $s->{coverage}{regions} // [] } ]; - $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; - $backend->{experimental} = 1; + $backend->{has_area} = $s->{coverage}{area} ? 1 : 0; + $backend->{association} = 1; if ( $s->{coverage}{area} and $s->{coverage}{area}{type} eq 'Polygon' + and defined $user_lon and $self->lonlat_in_polygon( $s->{coverage}{area}{coordinates}, [ $user_lon, $user_lat ] @@ -1099,7 +1130,8 @@ sub backend_form { push( @suggested_backends, $backend ); } elsif ( $s->{coverage}{area} - and $s->{coverage}{area}{type} eq 'MultiPolygon' ) + and $s->{coverage}{area}{type} eq 'MultiPolygon' + and defined $user_lon ) { for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } ) @@ -1160,6 +1192,7 @@ sub backend_form { if ( $s->{coverage}{area} and $s->{coverage}{area}{type} eq 'Polygon' + and defined $user_lon and $self->lonlat_in_polygon( $s->{coverage}{area}{coordinates}, [ $user_lon, $user_lat ] @@ -1169,7 +1202,8 @@ sub backend_form { push( @suggested_backends, $backend ); } elsif ( $s->{coverage}{area} - and $s->{coverage}{area}{type} eq 'MultiPolygon' ) + and $s->{coverage}{area}{type} eq 'MultiPolygon' + and defined $user_lon ) { for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } ) @@ -1211,6 +1245,7 @@ sub backend_form { if ( $s->{coverage}{area} and $s->{coverage}{area}{type} eq 'Polygon' + and defined $user_lon and $self->lonlat_in_polygon( $s->{coverage}{area}{coordinates}, [ $user_lon, $user_lat ] @@ -1220,7 +1255,8 @@ sub backend_form { push( @suggested_backends, $backend ); } elsif ( $s->{coverage}{area} - and $s->{coverage}{area}{type} eq 'MultiPolygon' ) + and $s->{coverage}{area}{type} eq 'MultiPolygon' + and defined $user_lon ) { for my $s_poly ( @{ $s->{coverage}{area}{coordinates} // [] } ) { @@ -1237,8 +1273,14 @@ sub backend_form { } } $backend->{type} = $type; + + $backend_by_id{ $backend->{id} } = $backend; } + my @frequent_backends = grep { $_->{type} } + map { $backend_by_id{$_} } + $self->journeys->get_frequent_backend_ids( uid => $user->{id} ); + @backends = map { $_->[1] } sort { $a->[0] cmp $b->[0] } map { [ lc( $_->{name} ), $_ ] } grep { $_->{type} } @backends; @@ -1246,6 +1288,7 @@ sub backend_form { $self->render( 'select_backend', suggestions => \@suggested_backends, + frequent => \@frequent_backends, backends => \@backends, user => $user, redirect_to => $self->req->param('redirect_to') // '/', diff --git a/lib/Travelynx/Controller/Api.pm b/lib/Travelynx/Controller/Api.pm index 572d3fa..fa40e76 100755 --- a/lib/Travelynx/Controller/Api.pm +++ b/lib/Travelynx/Controller/Api.pm @@ -188,10 +188,13 @@ sub travel_v1 { my $to_station = sanitize( q{}, $payload->{toStation} ); my $train_id; my $dbris = sanitize( undef, $payload->{dbris} ); + my $efa = sanitize( undef, $payload->{efa} ); my $hafas = sanitize( undef, $payload->{hafas} ); my $motis = sanitize( undef, $payload->{motis} ); - if ( not $hafas and exists $payload->{train}{journeyID} ) { + if ( not( $efa or $hafas or $motis ) + and exists $payload->{train}{journeyID} ) + { $dbris //= 'bahn.de'; } @@ -216,8 +219,7 @@ sub travel_v1 { return; } - if ( not $hafas - and not $dbris + if ( not( $dbris or $efa or $hafas or $motis ) and not $self->stations->search( $from_station, backend_id => 1 ) ) { $self->render( @@ -233,8 +235,7 @@ sub travel_v1 { } if ( $to_station - and not $hafas - and not $dbris + and not( $dbris or $efa or $hafas or $motis ) and not $self->stations->search( $to_station, backend_id => 1 ) ) { $self->render( @@ -297,8 +298,9 @@ sub travel_v1 { station => $from_station, train_id => $train_id, uid => $uid, - hafas => $hafas, dbris => $dbris, + efa => $efa, + hafas => $hafas, motis => $motis, ); } diff --git a/lib/Travelynx/Controller/Profile.pm b/lib/Travelynx/Controller/Profile.pm index db30d36..978e3f8 100755 --- a/lib/Travelynx/Controller/Profile.pm +++ b/lib/Travelynx/Controller/Profile.pm @@ -114,7 +114,8 @@ sub profile { my $map_data = {}; if ( $status->{checked_in} ) { $map_data = $self->journeys_to_map_data( - journeys => [$status], + journeys => [$status], + with_now_markers => 1, ); } @@ -506,7 +507,8 @@ sub user_status { my $map_data = {}; if ( $status->{checked_in} ) { $map_data = $self->journeys_to_map_data( - journeys => [$status], + journeys => [$status], + with_now_markers => 1, ); } @@ -600,7 +602,8 @@ sub status_card { if ( $status->{checked_in} ) { $map_data = $self->journeys_to_map_data( - journeys => [$status], + journeys => [$status], + with_now_markers => 1, ); } diff --git a/lib/Travelynx/Controller/Traveling.pm b/lib/Travelynx/Controller/Traveling.pm index 40802f4..5595e3c 100755 --- a/lib/Travelynx/Controller/Traveling.pm +++ b/lib/Travelynx/Controller/Traveling.pm @@ -8,13 +8,15 @@ use Mojo::Base 'Mojolicious::Controller'; use DateTime; use DateTime::Format::Strptime; +use GIS::Distance; use List::Util qw(uniq min max); use List::UtilsBy qw(max_by uniq_by); -use List::MoreUtils qw(first_index); +use List::MoreUtils qw(first_index last_index); use Mojo::UserAgent; use Mojo::Promise; use Text::CSV; use Travel::Status::DE::IRIS::Stations; +use XML::LibXML; # Internal Helpers @@ -370,7 +372,9 @@ sub homepage { my $map_data = {}; if ( $status->{arr_name} ) { $map_data = $self->journeys_to_map_data( - journeys => [$status], + journeys => [$status], + show_full_route => 1, + with_now_markers => 1, ); } my $journey_visibility @@ -460,7 +464,9 @@ sub status_card { my $map_data = {}; if ( $status->{arr_name} ) { $map_data = $self->journeys_to_map_data( - journeys => [$status], + journeys => [$status], + show_full_route => 1, + with_now_markers => 1, ); } my $journey_visibility @@ -588,13 +594,9 @@ sub geolocation { 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 - } + $self->dbris->geosearch_p( + latitude => $lat, + longitude => $lon )->then( sub { my ($dbris) = @_; @@ -605,7 +607,7 @@ sub geolocation { distance => 0, dbris => $dbris_service, } - } $dbris->results; + } uniq_by { $_->name } $dbris->results; if ( @results > 10 ) { @results = @results[ 0 .. 9 ]; } @@ -621,8 +623,13 @@ sub geolocation { $self->render( json => { candidates => [], - warning => $err, - } + error => $err, + }, + + # The frontend JavaScript does not have an XHR error handler yet + # (and if it did, I do not know whether it would have access to our JSON body). + # So, for now, we do the bad thing™ and return HTTP 200 even though the request to the backend was not successful. + # status => 502, ); } )->wait; @@ -665,8 +672,11 @@ sub geolocation { $self->render( json => { candidates => [], - warning => $err, - } + error => $err, + }, + + # See above + # status => 502 ); } )->wait; @@ -716,8 +726,11 @@ sub geolocation { $self->render( json => { candidates => [], - warning => $err, - } + error => $err, + }, + + # See above + #status => 502 ); } )->wait; @@ -730,6 +743,7 @@ sub geolocation { Travel::Status::MOTIS->new_p( promise => 'Mojo::Promise', user_agent => $self->ua, + time_zone => 'Europe/Berlin', service => $motis_service, stops_by_coordinate => { @@ -764,8 +778,11 @@ sub geolocation { $self->render( json => { candidates => [], - warning => $err, - } + error => $err, + }, + + # See above + #status => 502 ); } )->wait; @@ -1157,14 +1174,37 @@ sub station { $timestamp = DateTime->now( time_zone => 'Europe/Berlin' ); } - my $dbris_service = $self->param('dbris') - // ( $user->{backend_dbris} ? $user->{backend_name} : undef ); - my $efa_service = $self->param('efa') - // ( $user->{backend_efa} ? $user->{backend_name} : undef ); - my $hafas_service = $self->param('hafas') - // ( $user->{backend_hafas} ? $user->{backend_name} : undef ); - my $motis_service = $self->param('motis') - // ( $user->{backend_motis} ? $user->{backend_name} : undef ); + 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 @suggestions; + my $promise; if ($dbris_service) { if ( $station !~ m{ [@] L = \d+ }x ) { @@ -1266,6 +1306,37 @@ sub station { if ( $station =~ m{ [@] O = (?<name> [^@]+ ) [@] }x ) { $status->{station_name} = $+{name}; } + + my ($eva) = ( $station =~ m{ [@] L = (\d+) }x ); + my $backend_id + = $self->stations->get_backend_id( dbris => $dbris_service ); + my @destinations = $self->journeys->get_connection_targets( + uid => $uid, + backend_id => $backend_id, + eva => $eva + ); + + for my $dep (@results) { + destination: for my $dest (@destinations) { + if ( $dep->destination + and $dep->destination eq $dest->{name} ) + { + push( @suggestions, [ $dep, $dest ] ); + next destination; + } + for my $via_name ( $dep->via ) { + if ( $via_name eq $dest->{name} ) { + push( @suggestions, [ $dep, $dest ] ); + next destination; + } + } + } + } + + @suggestions = map { $_->[0] } + sort { $a->[1] <=> $b->[1] } + grep { $_->[1] >= $now - 300 } + map { [ $_, $_->[0]->dep->epoch ] } @suggestions; } elsif ($hafas_service) { @@ -1297,6 +1368,28 @@ sub station { station_name => $status->stop->full_name, related_stations => [], }; + my $backend_id + = $self->stations->get_backend_id( efa => $efa_service ); + my @destinations = $self->journeys->get_connection_targets( + uid => $uid, + backend_id => $backend_id, + eva => $status->{station_eva}, + ); + for my $dep (@results) { + destination: for my $dest (@destinations) { + for my $stop ( $dep->route_post ) { + if ( $stop->full_name eq $dest->{name} ) { + push( @suggestions, [ $dep, $dest ] ); + next destination; + } + } + } + } + + @suggestions = map { $_->[0] } + sort { $a->[1] <=> $b->[1] } + grep { $_->[1] >= $now - 300 and $_->[1] <= $now + 1800 } + map { [ $_, $_->[0]->datetime->epoch ] } @suggestions; } elsif ($motis_service) { @results = map { $_->[0] } @@ -1326,10 +1419,12 @@ sub station { my $user_status = $self->get_user_status; my $can_check_out = 0; + my ($eva) = ( $station =~ m{ [@] L = (\d+) }x ); + $eva //= $status->{station_eva}; if ( $user_status->{checked_in} ) { for my $stop ( @{ $user_status->{route_after} } ) { if ( - $stop->[1] eq $status->{station_eva} + $stop->[1] eq $eva or List::Util::any { $stop->[1] eq $_->{uic} } @{ $status->{related_stations} } ) @@ -1411,6 +1506,7 @@ sub station { related_stations => $status->{related_stations}, user_status => $user_status, can_check_out => $can_check_out, + suggestions => \@suggestions, title => "travelynx: $status->{station_name}", ); } @@ -1432,6 +1528,7 @@ sub station { related_stations => $status->{related_stations}, user_status => $user_status, can_check_out => $can_check_out, + suggestions => \@suggestions, title => "travelynx: $status->{station_name}", ); } @@ -1446,6 +1543,19 @@ sub station { status => 300, ); } + 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' ) @@ -1487,7 +1597,7 @@ sub station { )->wait; } elsif ( $err - =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden} + =~ m{svcRes|connection close|Service Temporarily Unavailable|Forbidden|HTTP 500 Internal Server Error|HTTP 429 Too Many Requests} ) { $self->render( @@ -1710,23 +1820,19 @@ sub map_history { my $with_polyline = $route_type eq 'beeline' ? 0 : 1; my $parser = DateTime::Format::Strptime->new( - pattern => '%d.%m.%Y', + pattern => '%F', locale => 'de_DE', time_zone => 'Europe/Berlin' ); - if ( $filter_from - and $filter_from =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x ) - { + if ($filter_from) { $filter_from = $parser->parse_datetime($filter_from); } else { $filter_from = undef; } - if ( $filter_until - and $filter_until =~ m{ ^ (\d+) [.] (\d+) [.] (\d+) $ }x ) - { + if ($filter_until) { $filter_until = $parser->parse_datetime($filter_until)->set( hour => 23, minute => 59, @@ -1804,15 +1910,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; @@ -1829,13 +1939,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} ) @@ -2080,11 +2194,17 @@ sub journey_details { $self->param( journey_id => $journey_id ); if ( not( $journey_id and $journey_id =~ m{ ^ \d+ $ }x ) ) { - $self->render( - 'journey', - status => 404, - error => 'notfound', - journey => {} + $self->respond_to( + json => { + json => { error => 'not found' }, + status => 404 + }, + any => { + template => 'journey', + status => 404, + error => 'notfound', + journey => {} + } ); return; } @@ -2100,6 +2220,40 @@ sub journey_details { ); if ($journey) { + + if ( $self->stash('polyline_export') ) { + + if ( not( $journey->{polyline} and @{ $journey->{polyline} } ) ) { + $journey->{polyline} + = [ map { [ $_->[2]{lon}, $_->[2]{lat}, $_->[1] ] } + @{ $journey->{route} } ]; + } + + delete $self->stash->{layout}; + + my $xml = $self->render_to_string( + template => 'polyline', + name => sprintf( '%s %s: %s → %s', + $journey->{type}, $journey->{no}, + $journey->{from_name}, $journey->{to_name} ), + polyline => $journey->{polyline} + ); + $self->respond_to( + gpx => { + text => $xml, + format => 'gpx' + }, + json => { + json => [ + map { + $_->[2] ? [ $_->[0], $_->[1], int( $_->[2] ) ] : $_ + } @{ $journey->{polyline} } + ] + }, + ); + return; + } + my $map_data = $self->journeys_to_map_data( journeys => [$journey], include_manual => 1, @@ -2137,29 +2291,39 @@ sub journey_details { $delay, $journey->{rt_arrival}->strftime('%H:%M') ); } - $self->render( - 'journey', - title => sprintf( - 'travelynx: Fahrt %s %s %s am %s', - $journey->{type}, $journey->{line} // '', - $journey->{no}, - $journey->{sched_departure}->strftime('%d.%m.%Y um %H:%M') - ), - error => undef, - journey => $journey, - journey_visibility => $visibility, - with_map => 1, - with_share => $with_share, - share_text => $share_text, - %{$map_data}, + $self->respond_to( + json => { json => $journey }, + any => { + template => 'journey', + title => sprintf( + 'travelynx: Fahrt %s %s %s am %s', + $journey->{type}, + $journey->{line} // '', + $journey->{no}, + $journey->{sched_departure}->strftime('%d.%m.%Y um %H:%M') + ), + error => undef, + journey => $journey, + journey_visibility => $visibility, + with_map => 1, + with_share => $with_share, + share_text => $share_text, + %{$map_data}, + } ); } else { - $self->render( - 'journey', - status => 404, - error => 'notfound', - journey => {} + $self->respond_to( + json => { + json => { error => 'not found' }, + status => 404 + }, + any => { + template => 'journey', + status => 404, + error => 'notfound', + journey => {} + } ); } @@ -2339,7 +2503,12 @@ sub edit_journey { my $error = undef; if ( $self->param('action') and $self->param('action') eq 'save' ) { - my $parser = DateTime::Format::Strptime->new( + my $parser_sec = DateTime::Format::Strptime->new( + pattern => '%d.%m.%Y %H:%M:%S', + locale => 'de_DE', + time_zone => 'Europe/Berlin' + ); + my $parser_min = DateTime::Format::Strptime->new( pattern => '%d.%m.%Y %H:%M', locale => 'de_DE', time_zone => 'Europe/Berlin' @@ -2350,7 +2519,8 @@ sub edit_journey { for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival)) { - my $datetime = $parser->parse_datetime( $self->param($key) ); + my $datetime = $parser_sec->parse_datetime( $self->param($key) ) + // $parser_min->parse_datetime( $self->param($key) ); if ( $datetime and $datetime->epoch ne $journey->{$key}->epoch ) { $error = $self->journeys->update( uid => $uid, @@ -2371,7 +2541,7 @@ sub edit_journey { uid => $uid, db => $db, id => $journey->{id}, - $key => $self->param($key) + $key => $self->param($key), ); if ($error) { last; @@ -2442,8 +2612,14 @@ sub edit_journey { for my $key (qw(sched_departure rt_departure sched_arrival rt_arrival)) { if ( $journey->{$key} and $journey->{$key}->epoch ) { - $self->param( - $key => $journey->{$key}->strftime('%d.%m.%Y %H:%M') ); + if ( $journey->{$key}->second ) { + $self->param( + $key => $journey->{$key}->strftime('%d.%m.%Y %H:%M:%S') ); + } + else { + $self->param( + $key => $journey->{$key}->strftime('%d.%m.%Y %H:%M') ); + } } } @@ -2463,17 +2639,228 @@ sub edit_journey { $self->render( 'edit_journey', with_autocomplete => 1, + backend_id => $journey->{backend_id}, error => $error, journey => $journey ); } +# Taken from Travel::Status::DE::EFA::Trip#polyline +sub polyline_add_stops { + my ( $self, %opt ) = @_; + + my $polyline = $opt{polyline}; + my $route = $opt{route}; + + my $distance = GIS::Distance->new; + + my %min_dist; + my $route_i = 0; + for my $stop ( @{$route} ) { + for my $polyline_index ( 0 .. $#{$polyline} ) { + my $pl = $polyline->[$polyline_index]; + if ( not( defined $stop->[2]{lat} and defined $stop->[2]{lon} ) ) { + my $err + = sprintf( +"Cannot match uploaded polyline with the journey's route: route stop %s (ID %s) has no lat/lon\n", + $stop->[0], $stop->[1] // 'unknown' ); + die($err); + } + my $dist + = $distance->distance_metal( $stop->[2]{lat}, $stop->[2]{lon}, + $pl->[1], $pl->[0] ); + my $key = $route_i . ';' . $stop->[1]; + if ( not $min_dist{$key} + or $min_dist{$key}{dist} > $dist ) + { + $min_dist{$key} = { + dist => $dist, + index => $polyline_index, + }; + } + } + $route_i += 1; + } + $route_i = 0; + for my $stop ( @{$route} ) { + my $key = $route_i . ';' . $stop->[1]; + if ( $min_dist{$key} ) { + if ( defined $polyline->[ $min_dist{$key}{index} ][2] ) { + return sprintf( +'Error: Route stops %d and %d both map to polyline lon/lat %f/%f. ' + . 'The uploaded polyline must cover the following route stops: %s', + $polyline->[ $min_dist{$key}{index} ][2], + $stop->[1], + $polyline->[ $min_dist{$key}{index} ][0], + $polyline->[ $min_dist{$key}{index} ][1], + join( + q{ · }, + map { + sprintf( + '%s (ID %s) @ %f/%f', + $_->[0], $_->[1] // 'unknown', + $_->[2]{lon}, $_->[2]{lat} + ) + } @{$route} + ), + ); + } + $polyline->[ $min_dist{$key}{index} ][2] + = $stop->[1]; + } + $route_i += 1; + } + return; +} + +sub set_polyline { + my ($self) = @_; + + if ( $self->validation->csrf_protect->has_error('csrf_token') ) { + $self->render( + 'bad_request', + csrf => 1, + status => 400 + ); + return; + } + + my $journey_id = $self->param('id'); + my $uid = $self->current_user->{id}; + + # Ensure that the journey exists and belongs to the user + my $journey = $self->journeys->get_single( + uid => $uid, + journey_id => $journey_id, + ); + + if ( not $journey ) { + $self->render( + 'bad_request', + message => 'Invalid journey ID', + status => 400, + ); + return; + } + + if ( my $upload = $self->req->upload('file') ) { + my $root; + eval { + $root = XML::LibXML->load_xml( string => $upload->asset->slurp ); + }; + + if ($@) { + $self->render( + 'bad_request', + message => "Invalid GPX file: Invalid XML: $@", + status => 400, + ); + return; + } + + my $context = XML::LibXML::XPathContext->new($root); + $context->registerNs( 'gpx', 'http://www.topografix.com/GPX/1/1' ); + + use Data::Dumper; + + my @polyline; + for my $point ( + $context->findnodes('/gpx:gpx/gpx:trk/gpx:trkseg/gpx:trkpt') ) + { + push( + @polyline, + [ + 0.0 + $point->getAttribute('lon'), + 0.0 + $point->getAttribute('lat') + ] + ); + } + + if ( not @polyline ) { + $self->render( + 'bad_request', + message => 'Invalid GPX file: found no track points', + status => 400, + ); + return; + } + + my @route = @{ $journey->{route} }; + + if ( $self->param('upload-partial') ) { + my $route_start = first_index { + ( + ( + $_->[1] and $_->[1] == $journey->{from_eva} + or $_->[0] eq $journey->{from_name} + ) + and ( + not( defined $_->[2]{sched_dep} + or defined $_->[2]{rt_dep} ) + or ( $_->[2]{sched_dep} // $_->[2]{rt_dep} ) + == $journey->{sched_dep_ts} + ) + ) + } + @route; + + my $route_end = last_index { + ( + ( + $_->[1] and $_->[1] == $journey->{to_eva} + or $_->[0] eq $journey->{to_name} + ) + and ( + not( defined $_->[2]{sched_arr} + or defined $_->[2]{rt_arr} ) + or ( $_->[2]{sched_arr} // $_->[2]{rt_arr} ) + == $journey->{sched_arr_ts} + ) + ) + } + @route; + + if ( $route_start > -1 and $route_end > -1 ) { + @route = @route[ $route_start .. $route_end ]; + } + } + + my $err = $self->polyline_add_stops( + polyline => \@polyline, + route => \@route, + ); + + if ($err) { + $self->render( + 'bad_request', + message => $err, + status => 400, + ); + return; + } + + $self->journeys->set_polyline( + uid => $uid, + journey_id => $journey_id, + edited => $journey->{edited}, + polyline => \@polyline, + from_eva => $route[0][1], + to_eva => $route[-1][1], + stats_ts => $journey->{rt_dep_ts}, + ); + } + + $self->redirect_to("/journey/${journey_id}"); +} + 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', + pattern => '%FT%H:%M', locale => 'de_DE', time_zone => 'Europe/Berlin' ); @@ -2493,7 +2880,7 @@ sub add_journey_form { 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; } @@ -2531,7 +2918,7 @@ sub add_journey_form { $opt{db} = $db; $opt{uid} = $self->current_user->{id}; - $opt{backend_id} = 1; + $opt{backend_id} = $self->current_user->{backend_id}; my ( $journey_id, $error ) = $self->journeys->add(%opt); @@ -2567,4 +2954,269 @@ 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 => '%FT%H:%M', + locale => 'de_DE', + time_zone => 'Europe/Berlin' + ); + my $time_parser = DateTime::Format::Strptime->new( + pattern => '%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_ts = $trip{sched_departure}; + for my $station ( @{ $trip{route} } ) { + my $ts; + my %station_data; + if ( $station + =~ m{ ^ (?<stop> [^@]+? ) \s* [@] \s* (?<timestamp> .+ ) $ }x + ) + { + $station = $+{stop}; + + # attempt to parse "07:08" short timestamp first + $ts = $time_parser->parse_datetime( $+{timestamp} ); + if ($ts) { + + # fill in last stop's (or at the first stop, our departure's) + # date to complete the datetime + $ts = $ts->set( + year => $prev_ts->year, + month => $prev_ts->month, + day => $prev_ts->day + ); + + # if we go back in time with this, assume we went + # over midnight and add a day, e.g. in case of a stop + # at 23:00 followed by one at 01:30 + if ( $ts < $prev_ts ) { + $ts = $ts->add( days => 1 ); + } + } + else { + # do a full datetime parse + $ts = $parser->parse_datetime( $+{timestamp} ); + } + if ( $ts and $ts >= $prev_ts ) { + $station_data{sched_arr} = $ts->epoch; + $station_data{sched_dep} = $ts->epoch; + $prev_ts = $ts; + } + 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 deleted file mode 100644 index a310aa3..0000000 --- a/lib/Travelynx/Helper/DBDB.pm +++ /dev/null @@ -1,201 +0,0 @@ -package Travelynx::Helper::DBDB; - -# Copyright (C) 2020-2023 Birte Kristina Friesel -# -# SPDX-License-Identifier: AGPL-3.0-or-later - -use strict; -use warnings; -use 5.020; - -use Encode qw(decode); -use Mojo::Promise; -use JSON; - -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 has_wagonorder_p { - 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 = $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) - ->get_p( $url => $self->{header} ) - ->then( - sub { - my ($tx) = @_; - if ( $tx->result->is_success ) { - $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 { - my $code = $tx->res->code; - $self->{log}->debug("${debug_prefix}: n (HTTP $code)"); - $self->{realtime_cache}->set( "HEAD $url", 'n' ); - $promise->reject; - } - return; - } - )->catch( - sub { - my ($err) = @_; - $self->{log}->debug("${debug_prefix}: n ($err)"); - $self->{realtime_cache}->set( "HEAD $url", 'n' ); - $promise->reject; - return; - } - )->wait; - return $promise; -} - -sub get_wagonorder_p { - 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 $promise = Mojo::Promise->new; - - 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} ) - ->then( - sub { - my ($tx) = @_; - - if ( $tx->result->is_success ) { - my $body = decode( 'utf-8', $tx->res->body ); - my $json = JSON->new->decode($body); - $self->{log}->debug("${debug_prefix}: success"); - $self->{main_cache}->freeze( $url, $json ); - $promise->resolve($json); - } - else { - my $code = $tx->res->code; - $self->{log}->debug("${debug_prefix}: HTTP ${code}"); - $promise->reject("HTTP ${code}"); - } - return; - } - )->catch( - sub { - my ($err) = @_; - $self->{log}->debug("${debug_prefix}: error ${err}"); - $promise->reject($err); - return; - } - )->wait; - return $promise; -} - -sub get_stationinfo_p { - my ( $self, $eva ) = @_; - - my $url = "https://lib.finalrewind.org/dbdb/s/${eva}.json"; - - 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} ) - ->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( - sub { - my ($err) = @_; - $self->{log}->debug("get_stationinfo_p(${eva}): Error ${err}"); - $cache->freeze( $url, {} ); - $promise->reject($err); - return; - } - )->wait; - return $promise; -} - -1; diff --git a/lib/Travelynx/Helper/DBRIS.pm b/lib/Travelynx/Helper/DBRIS.pm index 1b7f099..deeed65 100644 --- a/lib/Travelynx/Helper/DBRIS.pm +++ b/lib/Travelynx/Helper/DBRIS.pm @@ -29,18 +29,54 @@ sub new { return bless( \%opt, $class ); } +sub get_agent { + my ($self) = @_; + + 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); + } + + return $agent; +} + +sub geosearch_p { + my ( $self, %opt ) = @_; + + return Travel::Status::DE::DBRIS->new_p( + promise => 'Mojo::Promise', + user_agent => $self->get_agent, + geoSearch => \%opt, + developer_mode => $self->{log}->is_level('debug') ? 1 : 0, + ); +} + 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}, + cache => $self->{realtime_cache}, lwp_options => { timeout => 10, agent => $self->{header}{'User-Agent'}, }, - promise => 'Mojo::Promise', - user_agent => Mojo::UserAgent->new, + promise => 'Mojo::Promise', + user_agent => $self->get_agent, + developer_mode => $self->{log}->is_level('debug') ? 1 : 0, )->then( sub { my ($dbris) = @_; @@ -67,8 +103,6 @@ sub get_station_id_p { sub get_departures_p { my ( $self, %opt ) = @_; - my $agent = $self->{user_agent}; - if ( $opt{station} =~ m{ [@] L = (?<eva> \d+ ) }x ) { $opt{station} = { eva => $+{eva}, @@ -81,12 +115,15 @@ sub get_departures_p { ? $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), + station => $opt{station}, + datetime => $when, + num_vias => 42, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => $self->get_agent->request_timeout(10), + developer_mode => $self->{log}->is_level('debug') ? 1 : 0, ); } @@ -95,28 +132,13 @@ sub get_journey_p { 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), + journey => $opt{trip_id}, + with_polyline => $opt{with_polyline}, + cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => $self->get_agent->request_timeout(10), + developer_mode => $self->{log}->is_level('debug') ? 1 : 0, )->then( sub { my ($dbris) = @_; @@ -143,4 +165,25 @@ sub get_journey_p { return $promise; } +sub get_wagonorder_p { + my ( $self, %opt ) = @_; + + $self->{log} + ->debug("get_wagonorder_p($opt{train_type} $opt{train_no} @ $opt{eva})"); + + return Travel::Status::DE::DBRIS->new_p( + cache => $self->{main_cache}, + failure_cache => $self->{realtime_cache}, + promise => 'Mojo::Promise', + user_agent => $self->get_agent->request_timeout(10), + formation => { + departure => $opt{datetime}, + eva => $opt{eva}, + train_type => $opt{train_type}, + train_number => $opt{train_no} + }, + developer_mode => $self->{log}->is_level('debug') ? 1 : 0, + ); +} + 1; diff --git a/lib/Travelynx/Helper/Locales.pm b/lib/Travelynx/Helper/Locales.pm new file mode 100644 index 0000000..d5ccb71 --- /dev/null +++ b/lib/Travelynx/Helper/Locales.pm @@ -0,0 +1,28 @@ +package Travelynx::Helper::Locales; + +use strict; +use warnings; + +#BEGIN { package Locale::Maketext; sub DEBUG() {1} }; +#BEGIN { package Locale::Maketext::Guts; sub DEBUG() {1} }; + +use base qw(Locale::Maketext); + +# Uncomment this to show raw strings for untranslated content rather than +# falling back to German. + +#our %Lexicon = ( +# _AUTO => 1, +#); + +use Locale::Maketext::Lexicon { + _decode => 1, + '*' => [ Gettext => 'share/locales/*.po' ], +}; + +sub init { + my ($self) = @_; + return $self->SUPER::init( @_[ 1 .. $#_ ] ); +} + +1; diff --git a/lib/Travelynx/Helper/MOTIS.pm b/lib/Travelynx/Helper/MOTIS.pm index d4e1777..df79385 100644 --- a/lib/Travelynx/Helper/MOTIS.pm +++ b/lib/Travelynx/Helper/MOTIS.pm @@ -53,6 +53,7 @@ sub get_station_by_query_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'}, @@ -101,6 +102,7 @@ sub get_departures_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'}, @@ -125,6 +127,7 @@ sub get_trip_p { cache => $self->{realtime_cache}, promise => 'Mojo::Promise', user_agent => Mojo::UserAgent->new, + time_zone => 'Europe/Berlin', service => $opt{service}, trip_id => $opt{trip_id}, diff --git a/lib/Travelynx/Model/InTransit.pm b/lib/Travelynx/Model/InTransit.pm index b67b716..8324027 100644 --- a/lib/Travelynx/Model/InTransit.pm +++ b/lib/Travelynx/Model/InTransit.pm @@ -10,6 +10,7 @@ use warnings; use 5.020; use DateTime; +use GIS::Distance; use JSON; my %visibility_itoa = ( @@ -101,6 +102,7 @@ sub add { 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}; @@ -129,7 +131,7 @@ sub add { messages => $json->encode( [ map { [ $_->[0]->epoch, $_->[1] ] } $train->messages ] ), - data => JSON->new->encode( + data => $json->encode( { rt => $train->departure_has_realtime ? 1 : 0, @@ -152,19 +154,22 @@ sub add { $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], + 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 ), + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + platform => $j_stop->platform, + efa_load => $j_stop->occupancy, + lat => $j_stop->latlon->[0], + lon => $j_stop->latlon->[1], } ] ); + if ( $j_stop->is_cancelled ) { + $route[-1][2]{isCancelled} = 1; + } } $persistent_data->{operator} = $journey->operator; $db->insert( @@ -182,13 +187,13 @@ sub add { sched_departure => $stop->sched_dep, real_departure => $stop->rt_dep // $stop->sched_dep, route => $json->encode( \@route ), - data => JSON->new->encode( + data => $json->encode( { rt => $stop->rt_dep ? 1 : 0, %{ $data // {} } } ), - user_data => JSON->new->encode($persistent_data), + user_data => $json->encode($persistent_data), backend_id => $backend_id, } ); @@ -213,6 +218,7 @@ sub add { rt_dep => _epoch( $j_stop->rt_dep ), arr_delay => $j_stop->arr_delay, dep_delay => $j_stop->dep_delay, + platform => $j_stop->platform, load => $j_stop->load, lat => $j_stop->loc->lat, lon => $j_stop->loc->lon, @@ -243,13 +249,13 @@ sub add { sched_departure => $stop->{sched_dep}, real_departure => $stop->{rt_dep} // $stop->{sched_dep}, route => $json->encode( \@route ), - data => JSON->new->encode( + data => $json->encode( { rt => $stop->{rt_dep} ? 1 : 0, %{ $data // {} } } ), - user_data => JSON->new->encode($persistent_data), + user_data => $json->encode($persistent_data), backend_id => $backend_id, } ); @@ -258,7 +264,11 @@ sub add { and $stop and ref($journey) eq 'Travel::Status::DE::DBRIS::Journey' ) { - my $number = $journey->train_no // $journey->number // $train_suffix; + my $trip_no + = $journey->trip_no_at( $stop->eva, + $stop->sched_dep ? $stop->sched_dep->epoch : undef ) + // $journey->train_no; + my $number = $trip_no // $journey->number // $train_suffix; my $line; if ( defined $journey->line_no and $journey->line_no ne $number ) { @@ -276,14 +286,14 @@ sub add { $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 => { + 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 ), + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + platform => $j_stop->platform, + load => { FIRST => $j_stop->occupancy_first, SECOND => $j_stop->occupancy_second }, @@ -292,6 +302,12 @@ sub add { } ] ); + if ( $j_stop->is_additional ) { + $route[-1][2]{isAdditional} = 1; + } + if ( $j_stop->is_cancelled ) { + $route[-1][2]{isCancelled} = 1; + } } my @messages; for my $msg ( $journey->messages ) { @@ -313,6 +329,12 @@ sub add { ); } } + if ( scalar $journey->admin_ids ) { + $persistent_data->{admin_ids} = [ $journey->admin_ids ]; + } + if ( scalar $journey->operators ) { + $persistent_data->{operators} = [ $journey->operators ]; + } $db->insert( 'in_transit', { @@ -330,13 +352,13 @@ sub add { sched_departure => $stop->sched_dep, real_departure => $stop->rt_dep // $stop->sched_dep, route => $json->encode( \@route ), - data => JSON->new->encode( + data => $json->encode( { rt => $stop->{rt_dep} ? 1 : 0, %{ $data // {} } } ), - user_data => JSON->new->encode($persistent_data), + user_data => $json->encode($persistent_data), backend_id => $backend_id, } ); @@ -363,6 +385,7 @@ sub add { _epoch( $journey_stopover->realtime_departure ), arr_delay => $journey_stopover->arrival_delay, dep_delay => $journey_stopover->departure_delay, + platform => $journey_stopover->track, lat => $journey_stopover->stop->lat, lon => $journey_stopover->stop->lon, } @@ -389,17 +412,50 @@ sub add { sched_departure => $stopover->scheduled_departure, real_departure => $stopover->departure, route => $json->encode( \@route ), - data => JSON->new->encode( + data => $json->encode( { rt => $stopover->{is_realtime} ? 1 : 0, %{ $data // {} } } ), - user_data => JSON->new->encode($persistent_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('invalid arguments / argument types passed to InTransit->add'); } @@ -478,6 +534,14 @@ sub postprocess { $ret->{comment} = $ret->{user_data}{comment}; $ret->{wagongroups} = $ret->{user_data}{wagongroups}; + if ( $ret->{sched_dep_ts} and $ret->{real_dep_ts} ) { + $ret->{dep_delay} = $ret->{real_dep_ts} - $ret->{sched_dep_ts}; + } + + if ( $ret->{sched_arr_ts} and $ret->{real_arr_ts} ) { + $ret->{arr_delay} = $ret->{real_arr_ts} - $ret->{sched_arr_ts}; + } + $ret->{platform_type} = 'Gleis'; if ( $ret->{train_type} and $ret->{train_type} =~ m{ ast | bus | ruf }ix ) { $ret->{platform_type} = 'Steig'; @@ -595,6 +659,7 @@ sub get { if ( $opt{with_polyline} and $ret ) { $ret->{dep_latlon} = [ $ret->{dep_lat}, $ret->{dep_lon} ]; $ret->{arr_latlon} = [ $ret->{arr_lat}, $ret->{arr_lon} ]; + $ret->{now_latlon} = $self->estimate_trip_position($ret); } if ( $opt{with_visibility} and $ret ) { @@ -736,6 +801,22 @@ sub set_arrival_eva { ); } +sub set_arrival_platform { + my ( $self, %opt ) = @_; + + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $platform = $opt{arrival_platform}; + + $db->update( + 'in_transit', + { + arr_platform => $platform, + }, + { user_id => $uid } + ); +} + sub set_arrival_times { my ( $self, %opt ) = @_; @@ -1193,15 +1274,15 @@ sub update_arrival_dbris { $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 => { + 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, + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + platform => $j_stop->platform, + load => { FIRST => $j_stop->occupancy_first, SECOND => $j_stop->occupancy_second }, @@ -1210,6 +1291,12 @@ sub update_arrival_dbris { } ] ); + if ( $j_stop->is_additional ) { + $route[-1][2]{isAdditional} = 1; + } + if ( $j_stop->is_cancelled ) { + $route[-1][2]{isCancelled} = 1; + } } # selecting on user_id and train_no avoids a race condition if a user checks @@ -1218,8 +1305,8 @@ sub update_arrival_dbris { $db->update( 'in_transit', { - real_arrival => $stop->{rt_arr}, - arr_platform => $stop->{platform}, + real_arrival => $stop->rt_arr, + arr_platform => $stop->platform, route => $json->encode( [@route] ), data => $json->encode($ephemeral_data), user_data => $json->encode($persistent_data), @@ -1261,21 +1348,27 @@ sub update_arrival_efa { $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], + 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 ), + arr_delay => $j_stop->arr_delay, + dep_delay => $j_stop->dep_delay, + platform => $j_stop->platform, + efa_load => $j_stop->occupancy, + lat => $j_stop->latlon->[0], + lon => $j_stop->latlon->[1], } ] ); + if ( $j_stop->is_cancelled ) { + $route[-1][2]{isCancelled} = 1; + } } + # TODO set efa_load from old route entry if missing in current route entry + # (at least in VVO, occupancy data is only provided for future stops) + # 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. @@ -1284,6 +1377,7 @@ sub update_arrival_efa { { data => $json->encode($ephemeral_data), real_arrival => $stop->rt_arr, + arr_platform => $stop->platform, route => $json->encode( [@route] ), }, { @@ -1321,6 +1415,7 @@ sub update_arrival_motis { rt_dep => _epoch( $journey_stopover->realtime_departure ), arr_delay => $journey_stopover->arrival_delay, dep_delay => $journey_stopover->departure_delay, + platform => $journey_stopover->track, lat => $journey_stopover->stop->lat, lon => $journey_stopover->stop->lon, } @@ -1334,7 +1429,8 @@ sub update_arrival_motis { $db->update( 'in_transit', { - real_arrival => $stopover->{realtime_arrival}, + real_arrival => $stopover->realtime_arrival, + arr_platform => $stopover->track, route => $json->encode( [@route] ), }, { @@ -1380,6 +1476,7 @@ sub update_arrival_hafas { rt_dep => _epoch( $j_stop->rt_dep ), arr_delay => $j_stop->arr_delay, dep_delay => $j_stop->dep_delay, + platform => $j_stop->platform, load => $j_stop->load, lat => $j_stop->loc->lat, lon => $j_stop->loc->lon, @@ -1406,7 +1503,8 @@ sub update_arrival_hafas { 'in_transit', { data => $json->encode($ephemeral_data), - real_arrival => $stop->{rt_arr}, + real_arrival => $stop->rt_arr, + arr_platform => $stop->platform, route => $json->encode( [@route] ), }, { @@ -1488,4 +1586,141 @@ sub update_visibility { ); } +sub estimate_trip_position_between_stops { + my ( $self, %opt ) = @_; + + my $time_complete = $opt{now} - $opt{from_ts}; + my $time_total = $opt{to_ts} - $opt{from_ts}; + my $ratio = $time_complete / $time_total; + + my $distance = GIS::Distance->new; + my $polyline = $opt{polyline}; + my ( $i_from, $i_to ); + + for my $i ( 0 .. $#{$polyline} ) { + if ( not defined $i_from + and $polyline->[$i][2] + and $polyline->[$i][2] == $opt{from}[1] ) + { + $i_from = $i; + } + elsif ( not defined $i_to + and $polyline->[$i][2] + and $polyline->[$i][2] == $opt{to}[1] ) + { + $i_to = $i; + last; + } + } + if ( defined $i_from and defined $i_to ) { + my $total_distance = 0; + for my $i ( $i_from + 1 .. $i_to ) { + my $prev = $polyline->[ $i - 1 ]; + my $this = $polyline->[$i]; + if ( $prev and $this ) { + $total_distance + += $distance->distance_metal( $prev->[1], $prev->[0], + $this->[1], $this->[0] ); + } + } + + my $marker_distance = $total_distance * $ratio; + $total_distance = 0; + for my $i ( $i_from + 1 .. $i_to ) { + my $prev = $polyline->[ $i - 1 ]; + my $this = $polyline->[$i]; + if ( $prev and $this ) { + my $prev_distance = $total_distance; + $total_distance + += $distance->distance_metal( $prev->[1], $prev->[0], + $this->[1], $this->[0] ); + if ( $total_distance > $marker_distance ) { + my $sub_ratio = 1; + if ( $total_distance != $prev_distance ) { + $sub_ratio = ( $marker_distance - $prev_distance ) + / ( $total_distance - $prev_distance ); + } + return ( + $prev->[1] + ( $this->[1] - $prev->[1] ) * $sub_ratio, + $prev->[0] + ( $this->[0] - $prev->[0] ) * $sub_ratio, + ); + } + } + } + } + return ( + $opt{from}[2]{lat} + ( $opt{to}[2]{lat} - $opt{from}[2]{lat} ) * $ratio, + $opt{from}[2]{lon} + ( $opt{to}[2]{lon} - $opt{from}[2]{lon} ) * $ratio + ); +} + +sub estimate_trip_position { + my ( $self, $in_transit ) = @_; + + my @now_latlon; + my @route = @{ $in_transit->{route} }; + + # estimate_train_position runs before postprocess, so all route + # timestamps are provided in UNIX seconds and not as DateTime objects. + my $now = DateTime->now( time_zone => 'Europe/Berlin' )->epoch; + + my $prev_ts; + for my $i ( 0 .. $#route ) { + my $ts = $route[$i][2]{rt_arr} // $route[$i][2]{sched_arr} + // $route[$i][2]{rt_dep} // $route[$i][2]{sched_dep} // 0; + my $ts_dep = $route[$i][2]{rt_dep} // $route[$i][2]{sched_dep} + // $route[$i][2]{rt_arr} // $route[$i][2]{sched_arr} // 0; + if ( $ts and $ts_dep and $now >= $ts and $now <= $ts_dep ) { + + # Currently at a stop + @now_latlon = ( $route[$i][2]{lat}, $route[$i][2]{lon} ); + last; + } + if ( $ts + and $prev_ts + and $now > $prev_ts + and $now < $ts ) + { + @now_latlon = $self->estimate_trip_position_between_stops( + now => $now, + from => $route[ $i - 1 ], + from_ts => $prev_ts, + to => $route[$i], + to_ts => $ts, + polyline => $in_transit->{polyline}, + ); + last; + } + $prev_ts = $ts_dep; + } + + if ( not @now_latlon + and $in_transit->{sched_dep_ts} + and $in_transit->{sched_arr_ts} ) + { + my $time_complete = $now + - ( $in_transit->{real_dep_ts} // $in_transit->{sched_dep_ts} ); + my $time_total + = ( $in_transit->{real_arr_ts} // $in_transit->{sched_arr_ts} ) + - ( $in_transit->{real_dep_ts} // $in_transit->{sched_dep_ts} ); + + if ( $time_total == 0 ) { + return [ $in_transit->{dep_lat}, $in_transit->{dep_lon} ]; + } + + my $completion = $time_complete / $time_total; + $completion = $completion < 0 ? 0 : $completion > 1 ? 1 : $completion; + @now_latlon = ( + $in_transit->{dep_lat} + + ( $in_transit->{arr_lat} - $in_transit->{dep_lat} ) + * $completion, + $in_transit->{dep_lon} + + ( $in_transit->{arr_lon} - $in_transit->{dep_lon} ) + * $completion, + ); + } + + return \@now_latlon; +} + 1; diff --git a/lib/Travelynx/Model/Journeys.pm b/lib/Travelynx/Model/Journeys.pm index 0fb663e..bce475f 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 first_index last_index); my %visibility_itoa = ( 100 => 'public', @@ -50,6 +50,8 @@ sub epoch_to_dt { ); } +# TODO turn into a travelynx helper called from templates so that +# loc_handle is available for localization sub min_to_human { my ( $self, $minutes ) = @_; @@ -183,20 +185,44 @@ sub add { } 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 $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) { + $station_data{lat} = $station_info->{lat}; + $station_data{lon} = $station_info->{lon}; push( @route, [ - $station_info->{name}, - $station_info->{eva}, - { - lat => $station_info->{lat}, - lon => $station_info->{lon}, - } + $station_info->{name}, $station_info->{eva}, + \%station_data, ] ); } @@ -283,8 +309,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' } ) @@ -301,16 +333,16 @@ sub update { my $rows; my $journey = $self->get_single( - uid => $uid, - db => $db, - journey_id => $journey_id, - with_datetime => 1, - with_route_datetime => 1, + uid => $uid, + db => $db, + journey_id => $journey_id, + with_datetime => 1, ); eval { if ( exists $opt{from_name} ) { - my $from_station = $self->{stations}->search( $opt{from_name} ); + my $from_station = $self->{stations} + ->search( $opt{from_name}, backend_id => $journey->{backend_id} ); if ( not $from_station ) { die("Unbekannter Startbahnhof\n"); } @@ -326,7 +358,8 @@ sub update { )->rows; } if ( exists $opt{to_name} ) { - my $to_station = $self->{stations}->search( $opt{to_name} ); + my $to_station = $self->{stations} + ->search( $opt{to_name}, backend_id => $journey->{backend_id} ); if ( not $to_station ) { die("Unbekannter Zielbahnhof\n"); } @@ -399,7 +432,40 @@ sub update { )->rows; } if ( exists $opt{route} ) { - my @new_route = map { [ $_, undef, {} ] } @{ $opt{route} }; + + # If $opt{route} is a subset of $journey->{route}, we can recycle all data + my @new_route; + my $new_route_i = 0; + for my $old_route_i ( 0 .. $#{ $journey->{route} } ) { + if ( $journey->{route}[$old_route_i][0] eq + $opt{route}[$new_route_i] ) + { + $new_route_i += 1; + push( @new_route, $journey->{route}[$old_route_i] ); + } + } + + # Otherwise, fetch stop IDs so that polylines remain usable + if ( @new_route != @{ $opt{route} } ) { + my %stop + = map { $_->{name} => $_ } $self->{stations}->get_by_names( + backend_id => $journey->{backend_id}, + names => [ $opt{route} ] + ); + @new_route = map { + [ + $_, + $stop{$_}{eva}, + defined $stop{$_}{eva} + ? { + lat => $stop{$_}{lat}, + lon => $stop{$_}{lon} + } + : {} + ] + } @{ $opt{route} }; + } + $rows = $db->update( 'journeys', { @@ -537,6 +603,83 @@ sub pop { return $journey; } +sub set_polyline { + my ( $self, %opt ) = @_; + + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $polyline = $opt{polyline}; + + my $from_eva = $opt{from_eva}; + my $to_eva = $opt{to_eva}; + + my $polyline_str = JSON->new->encode($polyline); + + my $pl_res = $db->select( + 'polylines', + ['id'], + { + origin_eva => $from_eva, + destination_eva => $to_eva, + polyline => $polyline_str, + }, + { limit => 1 } + ); + + my $polyline_id; + if ( my $h = $pl_res->hash ) { + $polyline_id = $h->{id}; + } + else { + $polyline_id = $db->insert( + 'polylines', + { + origin_eva => $from_eva, + destination_eva => $to_eva, + polyline => $polyline_str + }, + { returning => 'id' } + )->hash->{id}; + } + if ($polyline_id) { + $self->set_polyline_id( + uid => $uid, + db => $db, + polyline_id => $polyline_id, + journey_id => $opt{journey_id}, + edited => $opt{edited}, + ); + $self->stats_cache->invalidate( + ts => epoch_to_dt( $opt{stats_ts} ), + db => $db, + uid => $uid + ); + } + +} + +sub set_polyline_id { + my ( $self, %opt ) = @_; + + my $uid = $opt{uid}; + my $db = $opt{db} // $self->{pg}->db; + my $polyline_id = $opt{polyline_id}; + my $journey_id = $opt{journey_id}; + my $edited = $opt{edited}; + + $db->update( + 'journeys', + { + polyline_id => $polyline_id, + edited => $edited | 0x0040 + }, + { + user_id => $uid, + id => $opt{journey_id} + } + ); +} + sub get { my ( $self, %opt ) = @_; @@ -549,7 +692,7 @@ sub get { my @select = ( - 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_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, @@ -613,12 +756,13 @@ sub get { is_motis => $entry->{is_motis}, backend_name => $entry->{backend_name}, backend_id => $entry->{backend_id}, - type => $entry->{train_type}, + 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}, @@ -626,6 +770,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}, @@ -659,12 +804,18 @@ 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} ) { - $stop->[2]{$k} = epoch_to_dt( $stop->[2]{$k} ); + $stop->[2]{"${k}_dt"} = epoch_to_dt( $stop->[2]{$k} ); } } } @@ -1023,6 +1174,8 @@ sub generate_missing_stats { my $stats_index = 0; + my %need_year; + for my $journey_index ( 0 .. $#journey_months ) { if ( $stats_index < @stats_months and $journey_months[$journey_index][0] @@ -1034,6 +1187,7 @@ sub generate_missing_stats { } else { my ( $year, $month ) = @{ $journey_months[$journey_index] }; + $need_year{$year} = 1; $self->get_stats( uid => $uid, db => $db, @@ -1043,6 +1197,14 @@ sub generate_missing_stats { ); } } + for my $year ( keys %need_year ) { + $self->get_stats( + uid => $uid, + db => $db, + year => $year, + write_only => 1 + ); + } } sub get_nav_months { @@ -1133,9 +1295,10 @@ sub sanity_check { . ' Stimmt das wirklich?'; } if ( $journey->{edited} & 0x0010 and not $lax ) { - my @unknown_stations - = $self->{stations} - ->grep_unknown( map { $_->[0] } @{ $journey->{route} } ); + my @unknown_stations = $self->{stations}->grep_unknown( + backend_id => $journey->{backend_id}, + names => [ map { $_->[0] } @{ $journey->{route} } ] + ); if (@unknown_stations) { return 'Unbekannte Station(en): ' . join( ', ', @unknown_stations ); } @@ -1150,9 +1313,11 @@ sub get_travel_distance { my $from = $journey->{from_name}; my $from_eva = $journey->{from_eva}; my $from_latlon = $journey->{from_latlon}; + my $from_ts = $journey->{sched_dep_ts} // $journey->{rt_dep_ts}; my $to = $journey->{to_name}; my $to_eva = $journey->{to_eva}; my $to_latlon = $journey->{to_latlon}; + my $to_ts = $journey->{sched_arr_ts} // $journey->{rt_arr_ts}; my $route_ref = $journey->{route}; my $polyline_ref = $journey->{polyline}; @@ -1206,31 +1371,85 @@ sub get_travel_distance { my $geo = GIS::Distance->new(); my $distance_beeline = $geo->distance_metal( @{$from_latlon}, @{$to_latlon} ); - my @route - = after_incl { ( $_->[1] and $_->[1] == $from_eva ) or $_->[0] eq $from } + + # A trip may pass the same stop multiple times. + # Thus, two criteria must be met to select the start/end of the actual route: + # * stop name or ID matches, and + # * one of: + # - arrival/departure time at the stop matches, or + # - the stop does not have arrival/departure time + # In the latter case, we still face the risk of selecting the wrong + # start/end stop. However, we have no way of finding the right one. As the + # majority of trips do not pass the same stop multiple times, it's better + # to risk having a few inaccurate distances than not calculating the + # distance for any journey that lacks sched_dep/rt_dep or + # sched_from/rt_from. + + my $route_start = first_index { + ( + ( $_->[1] and $_->[1] == $from_eva or $_->[0] eq $from ) + and ( not( defined $_->[2]{sched_dep} or defined $_->[2]{rt_dep} ) + or ( $_->[2]{sched_dep} // $_->[2]{rt_dep} ) == $from_ts ) + ) + } @{$route_ref}; - @route - = before_incl { ( $_->[1] and $_->[1] == $to_eva ) or $_->[0] eq $to } - @route; - if ( - @route < 2 - or ( $route[-1][0] ne $to - and ( not $route[-1][1] or $route[-1][1] != $to_eva ) ) - ) - { + # Here, we need to use last_index. In case of ring lines, the first index + # will not have sched_arr/rt_arr set, but we should not select it as route + # end... + my $route_end = last_index { + ( + ( $_->[1] and $_->[1] == $to_eva or $_->[0] eq $to ) + and ( not( defined $_->[2]{sched_arr} or defined $_->[2]{rt_arr} ) + or ( $_->[2]{sched_arr} // $_->[2]{rt_arr} ) == $to_ts ) + ) + } + @{$route_ref}; - # I AM ERROR + if ( not defined $route_start and defined $route_end ) { return ( 0, 0, $distance_beeline ); } - my @polyline = after_incl { $_->[2] and $_->[2] == $from_eva } + my %seen; + for my $stop ( @{$route_ref} ) { + if ( not defined $stop->[1] ) { + return ( 0, 0, $distance_beeline ); + } + $seen{ $stop->[1] } //= 1; + $stop->[2]{n} = $seen{ $stop->[1] }; + $seen{ $stop->[1] } += 1; + } + + # Assumption: polyline entries are always [lat, lon] or [lat, lon, stop ID] + %seen = (); + for my $entry ( @{ $polyline_ref // [] } ) { + if ( $entry->[2] ) { + $seen{ $entry->[2] } //= 1; + $entry->[3] = $seen{ $entry->[2] }; + $seen{ $entry->[2] } += 1; + } + } + + $journey->{route_dep_index} = $route_start; + $journey->{route_arr_index} = $route_end; + + my @route = @{$route_ref}[ $route_start .. $route_end ]; + + # Just like the route, the polyline may contain the same stop more than + # once. So we need to select based on the seen counter. + my $poly_start = first_index { + $_->[2] and $_->[2] == $from_eva and $_->[3] == $route[0][2]{n} + } + @{ $polyline_ref // [] }; + my $poly_end = first_index { + $_->[2] and $_->[2] == $to_eva and $_->[3] == $route[-1][2]{n} + } @{ $polyline_ref // [] }; - @polyline - = before_incl { $_->[2] and $_->[2] == $to_eva } @polyline; - # ensure that before_incl matched -- otherwise, @polyline is too long - if ( @polyline and $polyline[-1][2] == $to_eva ) { + if ( defined $poly_start and defined $poly_end ) { + $journey->{poly_dep_index} = $poly_start; + $journey->{poly_arr_index} = $poly_end; + my @polyline = @{$polyline_ref}[ $poly_start .. $poly_end ]; my $prev_station = shift @polyline; for my $station (@polyline) { $distance_polyline += $geo->distance_metal( @@ -1850,6 +2069,34 @@ sub get_latest_dest_ids { ); } +sub get_frequent_backend_ids { + my ( $self, %opt ) = @_; + + my $uid = $opt{uid}; + my $threshold = $opt{threshold} + // DateTime->now( time_zone => 'Europe/Berlin' )->subtract( months => 4 ); + my $limit = $opt{limit} // 5; + my $db = $opt{db} //= $self->{pg}->db; + + my $res = $db->select( + 'journeys', + 'count(*) as count, backend_id', + { + user_id => $uid, + real_departure => { '>', $threshold }, + }, + { + group_by => ['backend_id'], + order_by => { -desc => 'count' }, + limit => $limit, + } + ); + + my @backend_ids = $res->hashes->map( sub { shift->{backend_id} } )->each; + + return @backend_ids; +} + # Returns a listref of {eva, name} hashrefs for the specified backend. sub get_connection_targets { my ( $self, %opt ) = @_; @@ -1857,9 +2104,14 @@ sub get_connection_targets { my $uid = $opt{uid}; my $threshold = $opt{threshold} // 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}; + my $db = $opt{db} //= $self->{pg}->db; + my $min_count = $opt{min_count} // 3; + my $backend_id = $opt{backend_id}; + my $dest_id = $opt{eva}; + + $self->{log}->debug( +"get_connection_targets(uid => $uid, backend_id => $backend_id, dest_id => $dest_id)" + ); if ( $opt{destination_name} ) { return { @@ -1868,8 +2120,6 @@ sub get_connection_targets { }; } - my $backend_id = $opt{backend_id}; - if ( not $dest_id ) { ( $dest_id, $backend_id ) = $self->get_latest_dest_ids(%opt); } @@ -1900,10 +2150,15 @@ sub get_connection_targets { order_by => { -desc => 'count' } } ); - my @destinations - = $res->hashes->grep( sub { shift->{count} >= $min_count } ) - ->map( sub { shift->{dest} } ) - ->each; + my @all_destinations = $res->hashes->each; + my @destinations; + + while ( not @destinations and $min_count > 0 ) { + @destinations = map { $_->{dest} } + grep { $_->{count} >= $min_count } @all_destinations; + $min_count--; + } + @destinations = $self->{stations}->get_by_evas( backend_id => $opt{backend_id}, evas => [@destinations] diff --git a/lib/Travelynx/Model/Stations.pm b/lib/Travelynx/Model/Stations.pm index bf35d1a..6c647ec 100644 --- a/lib/Travelynx/Model/Stations.pm +++ b/lib/Travelynx/Model/Stations.pm @@ -205,6 +205,9 @@ sub add_or_update { ); return; } + if (not $stop->latlon) { + die('Backend Error: Stop "' . $stop->full_name . '" has no geo coordinates'); + } $opt{db}->insert( 'stations', { @@ -458,11 +461,16 @@ sub get_by_name { # Slow sub get_by_names { - my ( $self, @names ) = @_; + my ( $self, %opt ) = @_; - my @ret - = $self->{pg}->db->select( 'stations', '*', { name => { '=', \@names } } ) - ->hashes->each; + my @ret = $self->{pg}->db->select( + 'stations', + '*', + { + name => { '=', $opt{names} }, + source => $opt{backend_id} + } + )->hashes->each; return @ret; } @@ -503,12 +511,27 @@ sub search { # Slow sub grep_unknown { - my ( $self, @stations ) = @_; + my ( $self, %opt ) = @_; - my %station = map { $_->{name} => 1 } $self->get_by_names(@stations); - my @unknown_stations = grep { not $station{$_} } @stations; + my %station = map { $_->{name} => 1 } $self->get_by_names(%opt); + my @unknown_stations = grep { not $station{$_} } @{ $opt{names} }; return @unknown_stations; } +sub get_bahn_stationinfo { + my ( $self, %opt ) = @_; + $opt{db} //= $self->{pg}->db; + + my $res + = $opt{db} + ->select( 'bahn_platform_directions', ['data'], { eva => $opt{eva} } ) + ->expand->hash; + + if ($res) { + return $res->{data}; + } + return; +} + 1; diff --git a/lib/Travelynx/Model/Users.pm b/lib/Travelynx/Model/Users.pm index be9e80b..3ef7f33 100644 --- a/lib/Travelynx/Model/Users.pm +++ b/lib/Travelynx/Model/Users.pm @@ -216,6 +216,14 @@ sub set_backend { ); } +sub set_language { + my ( $self, %opt ) = @_; + $opt{db} //= $self->{pg}->db; + + $opt{db} + ->update( 'users', { language => $opt{language} }, { id => $opt{uid} } ); +} + sub set_privacy { my ( $self, %opt ) = @_; my $db = $opt{db} // $self->{pg}->db; @@ -413,7 +421,7 @@ sub get { my $user = $db->select( 'users_with_backend', - 'id, name, status, public_level, email, ' + 'id, name, status, public_level, email, language, ' . 'accept_follows, notifications, ' . 'extract(epoch from registered_at) as registered_at_ts, ' . 'extract(epoch from last_seen) as last_seen_ts, ' @@ -423,10 +431,11 @@ sub get { )->hash; if ($user) { return { - id => $user->{id}, - name => $user->{name}, - status => $user->{status}, - notifications => $user->{notifications}, + id => $user->{id}, + name => $user->{name}, + languages => [ split( qr{[|]}, $user->{language} // q{} ) ], + status => $user->{status}, + notifications => $user->{notifications}, accept_follows => $user->{accept_follows} == 2 ? 1 : 0, accept_follow_requests => $user->{accept_follows} == 1 ? 1 : 0, default_visibility => $user->{public_level} & 0x7f, diff --git a/public/service-worker.js b/public/service-worker.js index dac4ea9..7d80374 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,17 +1,17 @@ -const CACHE_NAME = 'static-cache-v92'; +const CACHE_NAME = 'static-cache-v99'; const FILES_TO_CACHE = [ '/favicon.ico', '/offline.html', - '/static/v92/css/light.min.css', - '/static/v92/css/dark.min.css', - '/static/v92/css/material-icons.css', - '/static/v92/fonts/MaterialIcons-Regular.woff2', - '/static/v92/fonts/MaterialIcons-Regular.woff', - '/static/v92/fonts/MaterialIcons-Regular.ttf', - '/static/v92/js/jquery-3.4.1.min.js', - '/static/v92/js/materialize.min.js', - '/static/v92/js/travelynx-actions.min.js', - '/static/v92/js/geolocation.min.js', + '/static/v99/css/light.min.css', + '/static/v99/css/dark.min.css', + '/static/v99/css/material-icons.css', + '/static/v99/fonts/MaterialIcons-Regular.woff2', + '/static/v99/fonts/MaterialIcons-Regular.woff', + '/static/v99/fonts/MaterialIcons-Regular.ttf', + '/static/v99/js/jquery-3.4.1.min.js', + '/static/v99/js/materialize.min.js', + '/static/v99/js/travelynx-actions.min.js', + '/static/v99/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 396b8c3..f6ec5cc 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 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.RUF,.dep-line.AST,.dep-line.NachtBus,.dep-line.Niederflurbus,.dep-line.Stadtbus,.dep-line.MetroBus,.dep-line.PlusBus,.dep-line.RegionalBus,.dep-line.ExpressBus,.dep-line.RufTaxi{background-color:#a3167e;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{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{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} + */.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,.dep-line.FERRY{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,.dep-line.Trm{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.ECD,.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.FR,.dep-line.TLK,.dep-line.EIC,.dep-line.EIP,.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,.dep-line.ICN{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],input[type=date],input[type=datetime-local],textarea{color:#fff} diff --git a/public/static/css/light.min.css b/public/static/css/light.min.css index 60d5cf6..712ae3c 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 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.RUF,.dep-line.AST,.dep-line.NachtBus,.dep-line.Niederflurbus,.dep-line.Stadtbus,.dep-line.MetroBus,.dep-line.PlusBus,.dep-line.RegionalBus,.dep-line.ExpressBus,.dep-line.RufTaxi{background-color:#a3167e;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{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{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} + */.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,.dep-line.FERRY{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,.dep-line.Trm{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.ECD,.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.FR,.dep-line.TLK,.dep-line.EIC,.dep-line.EIP,.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,.dep-line.ICN{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 4e9c11b..61412c0 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/v92/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */ + src: url(/static/v99/fonts/MaterialIcons-Regular.eot); /* For IE6-8 */ src: local('Material Icons'), local('MaterialIcons-Regular'), - url(/static/v92/fonts/MaterialIcons-Regular.woff2) format('woff2'), - url(/static/v92/fonts/MaterialIcons-Regular.woff) format('woff'), - url(/static/v92/fonts/MaterialIcons-Regular.ttf) format('truetype'); + url(/static/v99/fonts/MaterialIcons-Regular.woff2) format('woff2'), + url(/static/v99/fonts/MaterialIcons-Regular.woff) format('woff'), + url(/static/v99/fonts/MaterialIcons-Regular.ttf) format('truetype'); } .material-icons { diff --git a/public/static/js/geolocation.js b/public/static/js/geolocation.js index 9689239..2082e44 100644 --- a/public/static/js/geolocation.js +++ b/public/static/js/geolocation.js @@ -26,7 +26,7 @@ $(document).ready(function() { const parts = stop.split(';'); const [ eva, name, dbris, efa, hafas, motis ] = parts; - const node = $('<a class="tablerow" href="/s/' + eva + '?dbris=' + dbris + '&efa=' + efa + '&hafas=' + hafas + '&motis=' + motis + '"><span><i class="material-icons" aria-hidden="true">' + (dbris == '0' ? 'train' : 'directions') + '</i>' + name + '</span></a>'); + const node = $('<a class="tablerow" href="/s/' + eva + '?dbris=' + (dbris||0) + '&efa=' + (efa||0) + '&hafas=' + (hafas||0) + '&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'); }); @@ -49,24 +49,36 @@ $(document).ready(function() { $.each(data.candidates, function(i, candidate) { let node; - if (candidate.motis !== undefined) { - const { id, name, motis } = candidate; - - node = $('<a class="tablerow" href="/s/' + id + '?motis=' + motis + '"><span><i class="material-icons" aria-hidden="true">train</i>' + name + '</span></a>'); - } else if (candidate.efa !== undefined) { + 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 { + } 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">' + (hafas == '0' ? 'train' : 'directions') + '</i>' + name + '</span></a>'); + 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); + + node = $('<a class="tablerow" href="/s/' + eva + '"><span><i class="material-icons" aria-hidden="true">train</i>' + name + '</span></a>'); } node.click(function() { diff --git a/public/static/js/geolocation.min.js b/public/static/js/geolocation.min.js index c3f5ac3..0689baf 100644 --- a/public/static/js/geolocation.min.js +++ b/public/static/js/geolocation.min.js @@ -1 +1 @@ -$(document).ready(function(){function i(){return $("div.geolocation div.progress")}function e(e){var t=$("div.geolocation").data("backend");$.post("/geolocation",{lon:e.coords.longitude,lat:e.coords.latitude,backend:t},a)}function t(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,t,n){var a=$(document.createElement("div")),t=(a.attr("class","error"),a.text(t),$(document.createElement("strong"))),e=(t.text(e+" "),a.prepend(t),$("div.geolocation").append(a),$("div.geolocation").data("recent"));if(e){t=e.split("|");const c=$(document.createElement("p"));$.each(t,function(e,t){var[t,n,a,i,o,r]=t.split(";"),t=$('<a class="tablerow" href="/s/'+t+"?dbris="+a+"&efa="+i+"&hafas="+o+"&motis="+r+'"><span><i class="material-icons" aria-hidden="true">'+("0"==a?"train":"directions")+"</i>"+n+"</span></a>");t.click(function(){$("nav .preloader-wrapper").addClass("active")}),c.append(t)}),$("p.geolocationhint").text("Letzte Ziele:"),i().replaceWith(c)}else i().remove()},a=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,t){let n;var a,i,o;(n=void 0!==t.motis?({id:a,name:i,motis:o}=t,$('<a class="tablerow" href="/s/'+a+"?motis="+o+'"><span><i class="material-icons" aria-hidden="true">train</i>'+i+"</span></a>")):void 0!==t.efa?(a=t.eva,o=t.name,i=t.efa,t.distance.toFixed(1),$('<a class="tablerow" href="/s/'+a+"?efa="+i+'"><span><i class="material-icons" aria-hidden="true">directions</i>'+o+"</span></a>")):(a=t.eva,i=t.name,o=t.hafas,t.distance.toFixed(1),$('<a class="tablerow" href="/s/'+a+"?hafas="+o+'"><span><i class="material-icons" aria-hidden="true">'+("0"==o?"train":"directions")+"</i>"+i+"</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,t)}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))}); +$(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)+"&efa="+(i||0)+"&hafas="+(o||0)+"&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/manifest.json b/public/static/manifest.json index 9a87b37..c1b7367 100644 --- a/public/static/manifest.json +++ b/public/static/manifest.json @@ -3,27 +3,27 @@ "short_name": "Travelynx", "scope": "/", "icons": [{ - "src": "/static/v92/icons/icon-128x128.png", + "src": "/static/v99/icons/icon-128x128.png", "sizes": "128x128", "type": "image/png" }, { - "src": "/static/v92/icons/icon-144x144.png", + "src": "/static/v99/icons/icon-144x144.png", "sizes": "144x144", "type": "image/png" }, { - "src": "/static/v92/icons/icon-152x152.png", + "src": "/static/v99/icons/icon-152x152.png", "sizes": "152x152", "type": "image/png" }, { - "src": "/static/v92/icons/icon-192x192.png", + "src": "/static/v99/icons/icon-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/static/v92/icons/icon-256x256.png", + "src": "/static/v99/icons/icon-256x256.png", "sizes": "256x256", "type": "image/png" }, { - "src": "/static/v92/icons/icon-512x512.png", + "src": "/static/v99/icons/icon-512x512.png", "sizes": "512x512", "type": "image/png" }], diff --git a/public/static/v91 b/public/static/v98 index 945c9b4..945c9b4 120000 --- a/public/static/v91 +++ b/public/static/v98 diff --git a/public/static/v92 b/public/static/v99 index 945c9b4..945c9b4 120000 --- a/public/static/v92 +++ b/public/static/v99 diff --git a/sass/src/common/local.scss b/sass/src/common/local.scss index 76e19de..140f6c1 100644 --- a/sass/src/common/local.scss +++ b/sass/src/common/local.scss @@ -208,13 +208,24 @@ ul.route-history > li { width: fit-content; min-width: 6ch; margin: 0 auto; - - &.Bus, &.BUS, &.RUF, &.AST, &.NachtBus, &.Niederflurbus, &.Stadtbus, &.MetroBus, &.PlusBus, &.RegionalBus, &.ExpressBus, &.RufTaxi { + + &.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, &.Tram, &.TRAM, &.Str, &.Strb, &.STB, &.Straenbahn, &.NachtTram { + &.RUF, &.AST, &.RufTaxi, &.Rufbus, &.Linientaxi { + background-color: #ffd800; + color: black; + border-radius: 5rem; + padding: .2rem .5rem; + } + &.Fhre, &.Fh, &.Schiff, &.SCH, &.KAT, &.FERRY { + background-color: #309fd1; + border-radius: 5rem; + padding: .2rem .5rem; + } + &.STR, &.Tram, &.TRAM, &.Str, &.Strb, &.STB, &.Straenbahn, &.NachtTram, &.Stadtbahn, &.Niederflurstrab, &.Trm { background-color: #c5161c; border-radius: 5rem; padding: .2rem .5rem; @@ -224,7 +235,7 @@ ul.route-history > li { border-radius: 5rem; padding: .2rem .5rem; } - &.U, &.M, &.SUBWAY, &.U-Bahn { + &.U, &.M, &.SUBWAY, &.U-Bahn, &.UBAHN, &.Schw-B, &.Schwebebahn, &.H-Bahn { background-color: #014e8d; border-radius: 5rem; padding: .2rem .5rem; @@ -235,14 +246,18 @@ ul.route-history > li { &.RB, &.MEX, &.TER, &.R, &.REGIONAL_RAIL, &.Regionalzug, &.R-Bahn, &.BRB { background-color: #1f4a87; } + // BE / NL + &.ECD, // DE &.IC, &.ICE, &.EC, &.ECE, &.D, // CH &.IR, // FR &.TGV, &.OGV, &.EST, + // IT + &.FR, // PL - &.TLK, &.EIC, + &.TLK, &.EIC, &.EIP, // MOTIS &.HIGHSPEED_RAIL, &.LONG_DISTANCE { background-color: #ff0404; @@ -253,7 +268,7 @@ ul.route-history > li { &.RJ, &.RJX { background-color: #c63131; } - &.NJ, &.EN, &.NIGHT_RAIL { + &.NJ, &.EN, &.NIGHT_RAIL, &.ICN { background-color: #29255b; } &.WB { diff --git a/sass/src/dark/index.scss b/sass/src/dark/index.scss index 28c615b..310a8a4 100644 --- a/sass/src/dark/index.scss +++ b/sass/src/dark/index.scss @@ -18,6 +18,8 @@ html { input[type=email], input[type=text], input[type=password], +input[type=date], +input[type=datetime-local], textarea { color: $off-black; } diff --git a/share/locales/de_DE.po b/share/locales/de_DE.po new file mode 100644 index 0000000..a9d4870 --- /dev/null +++ b/share/locales/de_DE.po @@ -0,0 +1,623 @@ +msgid "" +msgstr "" +"Language: de-DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +# +# Global Strings +# + +msgid "strftime.datetime" +msgstr "%d.%m.%Y %H:%M %Z" + +msgid "button.register" +msgstr "Registrieren" + +msgid "button.login" +msgstr "Anmelden" + +msgid "button.logout" +msgstr "Abmelden" + +msgid "footer.imprint" +msgstr "Impressum" + +msgid "footer.privacy" +msgstr "Datenschutz" + +msgid "footer.legend" +msgstr "Legende" + +msgid "footer.colour-scheme" +msgstr "Farbschema" + +msgid "footer.colour-scheme.light" +msgstr "hell" + +msgid "footer.colour-scheme.dark" +msgstr "dunkel" + +msgid "footer.colour-scheme.auto" +msgstr "automatisch" + +msgid "header.error" +msgstr "Fehler" + +# +# Templates +# + +# about.html.ep + +msgid "about.developed-by.lead" +msgstr "Entwickelt von" + +msgid "about.developed-by.and" +msgstr "und" + +msgid "about.developed-by.others" +msgstr "weiteren" + +msgid "about.developed-by.tail" +msgstr " " + +msgid "about.source-code" +msgstr "Quelltext" + +msgid "about.licence-agplv3" +msgstr "lizensiert unter AGPL v3" + +msgid "about.data-sources" +msgstr "Backends" + +msgid "about.data-sources.last-and" +msgstr " und" + +msgid "about.disclaimer" +msgstr "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." + +msgid "about.contact" +msgstr "Kontakt" + +msgid "about.bugs" +msgstr "Bugs" + +msgid "about.changelog" +msgstr "Änderungen" + +# account.html.ep + +msgid "account.changed-name" +msgstr "Name geändert" + +msgid "account.changed-mail" +msgstr "Mail-Adresse geändert" + +msgid "account.changed-password" +msgstr "Passwort geändert" + +msgid "account.changed-language" +msgstr "Sprache geändert" + +msgid "account.changed-privacy" +msgstr "Einstellungen zu öffentlichen Account-Daten geändert" + +msgid "account.changed-social" +msgstr "Einstellungen zur Interaktionen mit anderen Accounts geändert" + +msgid "account.changed-traewelling" +msgstr "Träwelling-Verknüpfung aktualisiert" + +msgid "account.changed-history" +msgstr "Einstellungen zu vorgeschlagenen Verbindungen geändert" + +msgid "account.changed-webhook" +msgstr "Web Hook aktualisiert" + +msgid "account.cleared-notifications" +msgstr "Benachrichtigungen gelesen" + +msgid "account.account" +msgstr "Account" + +msgid "account.name" +msgstr "Name" + +msgid "account.mail" +msgstr "E-Mail" + +msgid "account.password" +msgstr "Passwort" + +msgid "account.language" +msgstr "Sprache" + +msgid "account.connections" +msgstr "Verbindungen" + +msgid "account.connections.enabled" +msgstr "Vorschläge aktiv" + +msgid "account.connections.disabled" +msgstr "Vorschläge deaktiviert" + +msgid "account.visibility" +msgstr "Sichtbarkeit" + +msgid "account.webhook" +msgstr "Webhook" + +msgid "account.webhook.disabled" +msgstr "Nicht eingerichtet" + +msgid "account.webhook.active-pending" +msgstr "Aktiv, noch nicht ausgeführt" + +msgid "account.webhook.active-error" +msgstr "Aktiv, fehlerhaft" + +msgid "account.webhook.active" +msgstr "Aktiv" + +msgid "account.traewelling.unsupported" +msgstr "Wird wegen Inkompatibilität zwischen bahn.de und transitous derzeit nicht unterstützt" + +msgid "account.registration-date" +msgstr "Registriert am" + +msgid "account.interaction" +msgstr "Interaktion" + +msgid "account.interaction.accept-follows" +msgstr "Accounts können dir direkt folgen" + +msgid "account.interaction.accept-follow-requests" +msgstr "Accounts können dir auf Anfrage folgen" + +msgid "account.interaction.one" +msgstr "eine" + +msgid "account.interaction.open-request" +msgstr "offene Anfrage" + +msgid "account.interaction.open-requests" +msgstr "offene Anfragen" + +msgid "account.interaction.disabled" +msgstr "Accounts können dir nicht folgen" + +msgid "account.profile" +msgstr "Öffentliches Profil" + +msgid "account.interaction.requests" +msgstr "Offene anfragen" + +msgid "account.interaction.requests.incoming.none" +msgstr "keine eingehend" + +msgid "account.interaction.requests.incoming.some" +msgstr "eingehend" + +msgid "account.interaction.requests.outgoing.none" +msgstr "keine ausgehend" + +msgid "account.interaction.requests.outgoing.some" +msgstr "ausgehend" + +# changelog.html.ep + +msgid "changelog.added" +msgstr "Neues Feature" + +msgid "changelog.bugfix" +msgstr "Bugfix" + +msgid "changelog.2-16.1" +msgstr "Infrastruktur zur Übersetzung von travelynx in andere Sprachen. Der Großteil der Website ist noch Deutsch; einzelne Seiten sind bereits auf Englisch und Teilmengen davon zusätzlich auf Französisch, Polnisch und Ungarisch verfügbar. Weitere Übersetzungen und ggf. Sprachen sowie Dokumentation zur Unterstützung dabei folgen bei Zeiten. Die Sprache wird auf Basis des Accept-Language-Headers ausgewählt und kann zusätzlich in den Account-Einstellungen konfiguriert werden." + +msgid "changelog.2-16.2" +msgstr "Korrekte Berechnung und Visualisierung der Wegstrecke bei Fahrten mit Ringlinien. Die Anzeige der Fahrten in der Fahrtenkarte ist noch teilweise fehlerhaft." + +msgid "changelog.2-17.1" +msgstr "Kartendaten („Polylines“) zu einzelnen Fahrten können nun als GPX ex- und importiert werden. Somit können Kartendaten nachträglich korrigiert oder nachgepflegt werden. Das GPX-Datenformat ist mit BRouter-Web kompatibel." + +# journey.html.ep + +msgid "journey.not-found" +msgstr "Fahrt nicht gefunden." + +#, short +msgid "journey.trip" +msgstr "Fahrt" + +#, short +msgid "journey.from" +msgstr "Von" + +#, short +msgid "journey.to" +msgstr "Nach" + +#, short +msgid "journey.departure" +msgstr "Abfahrt" + +#, short +msgid "journey.arrival" +msgstr "Ankunft" + +#, short +msgid "journey.distance" +msgstr "Strecke" + +msgid "journey.beeline.pre" +msgstr "(Luftlinie: " + +msgid "journey.beeline.post" +msgstr ")" + +#, short +msgid "journey.speed" +msgstr "Tempo" + +#, short +msgid "journey.operator" +msgstr "Betrieb" + +#, short +msgid "journey.messages" +msgstr "Meldungen" + +#, short +msgid "journey.comment" +msgstr "Kommentar" + +#, short +msgid "journey.carriages" +msgstr "Rollmaterial" + +#, short +msgid "journey.route" +msgstr "Route" + +msgid "journey.share" +msgstr "Teilen" + +msgid "journey.export" +msgstr "Exportieren" + +msgid "journey.edit" +msgstr "Bearbeiten" + +msgid "journey.map-data" +msgstr "Kartendaten" + +msgid "journey.map.download" +msgstr "Herunterladen" + +msgid "journey.map.upload" +msgstr "Hochladen" + +msgid "journey.map.upload-full" +msgstr "Komplette Route" + +msgid "journey.map.upload-partial" +msgstr "Gefahrenes Segment" + +msgid "journey.map.info.download" +msgstr "JSON-Format: [[lon, lat, ID], ...] in WGS84-Koordinaten. GPX-Dateien sind mit BRouter kompatibel." + +msgid "journey.map.info.upload" +msgstr "GPX-Uploads müssen ein einzelnes track-Element mit einem einzelnen track segment enthalten. Ein BRouter-GPX-Export erfüllt diese Vorgaben. Uploads müssen entweder die komplette Route des Verkehrsmittels oder nur den zu diesem Checkin zugehörigen Abschnitt enthalten. Beim Hochladen bitte die passende Schaltfläche auswählen. IDs von Halten müssen beim Upload nicht angegeben werden. Bitte beachten: Beim Einspielen eigener Kartendaten werden die zuvor gespeicherten unwiderruflich gelöscht." + +msgid "journey.danger" +msgstr "Danger Zone" + +msgid "journey.delete" +msgstr "Löschen" + +# landingpage.html.ep + +msgid "landingpage.greeting-prefix" +msgstr "Hallo," + +msgid "landingpage.greeting-suffix" +msgstr "!" + +msgid "landingpage.not-checked-in" +msgstr "Du bist gerade nicht eingecheckt" + +msgid "landingpage.stop-geosearch" +msgstr "Stationen in der Umgebung suchen" + +msgid "landingpage.manual-stop-entry" +msgstr "Manuelle Eingabe" + +#, As short as possible +msgid "landingpage.departures" +msgstr "Abfahrten" + +msgid "landingpage.latest-trips" +msgstr "Letzte Fahrten" + +msgid "landingpage.date-format" +msgstr "%d.%m.%Y" + +msgid "landingpage.about" +msgstr "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." + +msgid "landingpage.traewelling.pre" +msgstr "Die Idee dazu kommt von" + +msgid "landingpage.traewelling.post" +msgstr " " + +msgid "landingpage.features" +msgstr "Features:" + +msgid "landingpage.features.log" +msgstr "Protokoll von Fahrplan- und Echtzeitdaten an Start- und Zielbahnhof" + +msgid "landingpage.features.share" +msgstr "Teilen von aktuellen und vergangenen Fahrten mit anderen Personen" + +msgid "landingpage.features.api-pre" +msgstr "Web-Hooks und" + +msgid "landingpage.features.api-link" +msgstr "API" + +msgid "landingpage.features.api-post" +msgstr "zum automatisierten Einchecken und Auslesen des aktuellen Status" + +msgid "landingpage.features.stats" +msgstr "Statistiken über Reisezeiten und Verspätungen" + +msgid "landingpage.features.passenger-rights" +msgstr "Unterstützung beim Ausfüllen von Fahrgastrechteformularen" + +msgid "landingpage.features.public" +msgstr "Optional: Öffentlicher Reisestatus und öffentliche Angaben zu vergangenen Fahrten" + +msgid "landingpage.disclaimer.lead" +msgstr "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." + +msgid "landingpage.disclaimer.source-pre" +msgstr "Wer mag, kann auch den" + +msgid "landingpage.disclaimer.source-link" +msgstr "Quelltext" + +msgid "landingpage.disclaimer.source-post" +msgstr "laden und eine eigene Instanz aufsetzen." + +# language.html.ep + +msgid "language.language" +msgstr "Sprache" + +msgid "language.browser-default" +msgstr "Gleiche Sprache wie Web-Browser" + +# login.html.ep + +msgid "login.accept-tos-pre" +msgstr "Mit der Anmeldung stimmst du den" + +msgid "login.tos" +msgstr "Nutzungsbedingungen" + +msgid "login.accept-tos-post" +msgstr "zu." + +msgid "login.forgot-password" +msgstr "Passwort vergessen" + +msgid "login.registration-disabled" +msgstr "Diese Instanz erlaubt derzeit keine Registrierung neuer Accounts" + +# register.html.ep + +msgid "register.name" +msgstr "Name (alphanumerisch)" + +msgid "register.mail" +msgstr "E-Mail-Adresse" + +msgid "register.password" +msgstr "Passwort" + +msgid "register.repeat-password" +msgstr "Passwort wiederholen" + +msgid "register.accept-tos-pre" +msgstr "Mit deiner Registrierung stimmst du den" + +msgid "register.tos" +msgstr "Nutzungsbedingungen" + +msgid "register.accept-tos-post" +msgstr "zu." + +msgid "register.expect-confirmation-link" +msgstr "Nach der Registrierung wird ein für 48 Stunden gültiger Bestätigungslink an die angegebene Mail-Adresse geschickt. Eine Anmeldung ist erst nach Bestätigung der Mail-Adresse möglich." + +msgid "register.why-mail" +msgstr "Die Mail-Adresse wird ausschließlich zur Bestätigung der Anmeldung, für die „Passwort vergessen“-Funktionalität und für wichtige Informationen über den Account verwendet und nicht an Dritte weitergegeben." + +msgid "register.privacy-pre" +msgstr "Die" + +msgid "register.privacy" +msgstr "Datenschutzerklärung" + +msgid "register.privacy-post" +msgstr "beschreibt weitere erhobene Daten sowie deren Zweck und Speicherfristen." + +msgid "register.account-deletion" +msgstr "Accounts werden nach einem Jahr ohne Aktivität per E-Mail über die bevorstehende Löschung informiert und nach vier weiteren Wochen ohne Aktivität automatisch gelöscht." + +msgid "register.disclaimer" +msgstr "Bitte beachten: Travelynx ist ein privat betriebenes Projekt ohne Verfügbarkeitsgarantie. Unangekündigte Downtimes oder eine kurzfristige Einstellung dieser Seite sind nicht vorgesehen, aber möglich." + +# _checked_in.html.ep, _public_status_card.html.ep + +msgid "status.is-checked-in" +msgstr "ist unterwegs" + +msgid "status.is-not-checked-in" +msgstr "ist gerade nicht eingecheckt" + +msgid "status.select-destination" +msgstr "Ziel wählen" + +msgid "status.share" +msgstr "Teilen" + +msgid "status.check-out" +msgstr "Auschecken" + +#, noun or verb +msgid "status.boarding-in.pre" +msgstr "Einfahrt in" + +#, noun or verb +msgid "status.boarding-in.post" +msgstr " " + +#, noun or verb +msgid "status.boarding-soon" +msgstr "fährt ein" + +#, noun or verb +msgid "status.departure-in.pre" +msgstr "Abfahrt in" + +#, noun or verb +msgid "status.departure-in.post" +msgstr " " + +#, noun or verb +msgid "status.departure-soon" +msgstr "fährt ab" + +#, noun or verb +msgid "status.arrival-in.pre" +msgstr "Ankunft in" + +#, noun or verb +msgid "status.arrival-in.post" +msgstr " " + +#, noun or verb +msgid "status.arrival-soon" +msgstr "Ankunft in weniger als einer Minute" + +msgid "status.arrival-unknown" +msgstr "Ankunft unbekannt" + +msgid "status.arrived" +msgstr "Ziel erreicht" + +msgid "status.depart-from.pre" +msgstr "von" + +msgid "status.depart-from.post" +msgstr " " + +msgid "status.arrive-on.pre" +msgstr "auf" + +msgid "status.arrive-on.post" +msgstr " " + +msgid "status.delayed-auto-checkout" +msgstr "Der automatische Checkout erfolgt spätestens eine halbe Stunde nach der Ankunft." + +msgid "status.realtime-unavailable" +msgstr "Keine Echtzeitdaten vorhanden" + +#, service messages related to the trip +msgid "status.messages" +msgstr "Meldungen" + +msgid "status.map" +msgstr "Karte" + +msgid "status.change-destination" +msgstr "Ziel ändern?" + +msgid "status.carriages" +msgstr "Wagen" + +msgid "status.route" +msgstr "Route" + +#, shown during destination selection +msgid "status.undo" +msgstr "Rückgängig" + +#, shown during destination selection +msgid "status.privacy-level" +msgstr "Sichtbarkeit" + +#, shown once check-in is completed +msgid "status.undo-checkin" +msgstr "Checkin rückgängig" + +msgid "status.force-checkout-lead" +msgstr "Maximal eine halbe Stunde nach der Ankunft erfolgt ein automatischer Checkout. Falls das Backend ausgefallen ist oder die Fahrt aus anderen Gründen verloren ging:" + +msgid "status.force-checkout" +msgstr "Jetzt auschecken" + +# _history_stats.html.ep + +msgid "stats.trips" +msgstr "Fahrten" + +msgid "stats.distance" +msgstr "Entfernung" + +msgid "stats.time" +msgstr "Fahrtzeit" + +msgid "stats.hours" +msgstr "Stunden" + +msgid "stats.per-schedule" +msgstr "nach Fahrplan" + +msgid "stats.transfer-time" +msgstr "Wartezeit (Umstiege)" + +msgid "stats.total-delay" +msgstr "Kumulierte Verspätung" + +msgid "stats.on-dep" +msgstr "Bei Abfahrt" + +msgid "stats.on-arr" +msgstr "Bei Ankunft" + +# _wagons.html.ep + +msgid "wagons.name-as-type" +msgstr "als" + +msgid "wagons.from.pre" +msgstr "von" + +msgid "wagons.from.post" +msgstr " " + +msgid "wagons.to.pre" +msgstr "nach" + +msgid "wagons.to.post" +msgstr " " + +msgid "wagons.carriage" +msgstr "Wagen" diff --git a/share/locales/en_GB.po b/share/locales/en_GB.po new file mode 100644 index 0000000..abe5f1d --- /dev/null +++ b/share/locales/en_GB.po @@ -0,0 +1,623 @@ +msgid "" +msgstr "" +"Language: en-GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +# +# Global Strings +# + +msgid "strftime.datetime" +msgstr "%Y-%m-%d %H:%M %Z" + +msgid "button.register" +msgstr "Register" + +msgid "button.login" +msgstr "Login" + +msgid "button.logout" +msgstr "Logout" + +msgid "footer.imprint" +msgstr "Imprint" + +msgid "footer.privacy" +msgstr "Privacy" + +msgid "footer.legend" +msgstr "Legend" + +msgid "footer.colour-scheme" +msgstr "Display Mode" + +msgid "footer.colour-scheme.light" +msgstr "light" + +msgid "footer.colour-scheme.dark" +msgstr "dark" + +msgid "footer.colour-scheme.auto" +msgstr "auto" + +msgid "header.error" +msgstr "Error" + +# +# Templates +# + +# about.html.ep + +msgid "about.developed-by.lead" +msgstr "Developed by" + +msgid "about.developed-by.and" +msgstr "and" + +msgid "about.developed-by.others" +msgstr "others" + +msgid "about.developed-by.tail" +msgstr " " + +msgid "about.source-code" +msgstr "Source code" + +msgid "about.licence-agplv3" +msgstr "available under the terms of AGPL v3" + +msgid "about.data-sources" +msgstr "Backends" + +msgid "about.data-sources.last-and" +msgstr ", and" + +msgid "about.disclaimer" +msgstr "Travelynx is a hobby project. It is provided free of charge, without any kind of availability guarantees. Unexpected downtimes or a cancellation of the entire site on short notice are not planned, but always possible. Depending on available spare time and motivation, feature requests, bug reports, and other messages are processed promptly, with delay, or not at all." + +msgid "about.contact" +msgstr "Contact" + +msgid "about.bugs" +msgstr "Bugs" + +msgid "about.changelog" +msgstr "Changelog" + +# account.html.ep + +msgid "account.changed-name" +msgstr "Updated name" + +msgid "account.changed-mail" +msgstr "Updated email address" + +msgid "account.changed-password" +msgstr "Updated password" + +msgid "account.changed-language" +msgstr "Changed language" + +msgid "account.changed-privacy" +msgstr "Privacy settings have been saved" + +msgid "account.changed-social" +msgstr "Social settings have been saved" + +msgid "account.changed-traewelling" +msgstr "Träwelling settings have been saved" + +msgid "account.changed-history" +msgstr "Connection suggestion settings have been saved" + +msgid "account.changed-webhook" +msgstr "Web Hook has been updated" + +msgid "account.cleared-notifications" +msgstr "Notifications have been cleared" + +msgid "account.account" +msgstr "Account" + +msgid "account.name" +msgstr "Name" + +msgid "account.mail" +msgstr "E-Mail" + +msgid "account.password" +msgstr "Password" + +msgid "account.language" +msgstr "Language" + +msgid "account.connections" +msgstr "Connections" + +msgid "account.connections.enabled" +msgstr "Suggestions enabled" + +msgid "account.connections.disabled" +msgstr "Suggestions disabled" + +msgid "account.visibility" +msgstr "Visibility" + +msgid "account.webhook" +msgstr "Webhook" + +msgid "account.webhook.disabled" +msgstr "No webhook configured" + +msgid "account.webhook.active-pending" +msgstr "Active; pending" + +msgid "account.webhook.active-error" +msgstr "Active; erroneous" + +msgid "account.webhook.active" +msgstr "Active" + +msgid "account.traewelling.unsupported" +msgstr "Unsupported due to incompatibility with bahn.de and transitous" + +msgid "account.registration-date" +msgstr "Registered on" + +msgid "account.interaction" +msgstr "Interaction" + +msgid "account.interaction.accept-follows" +msgstr "Accounts may follow you" + +msgid "account.interaction.accept-follow-requests" +msgstr "Accounts may send follow requests" + +msgid "account.interaction.one" +msgstr "one" + +msgid "account.interaction.open-request" +msgstr "open request" + +msgid "account.interaction.open-requests" +msgstr "open requests" + +msgid "account.interaction.disabled" +msgstr "Accounts cannot follow you" + +msgid "account.profile" +msgstr "Public profile page" + +msgid "account.interaction.requests" +msgstr "Pending requests" + +msgid "account.interaction.requests.incoming.none" +msgstr "none incoming" + +msgid "account.interaction.requests.incoming.some" +msgstr "incoming" + +msgid "account.interaction.requests.outgoing.none" +msgstr "none outgoing" + +msgid "account.interaction.requests.outgoing.some" +msgstr "outgoing" + +# changelog.html.ep + +msgid "changelog.added" +msgstr "New Feature" + +msgid "changelog.bugfix" +msgstr "Bugfix" + +msgid "changelog.2-16.1" +msgstr "Localization support. Parts of travelynx are now available in English, and a subset of those is also available in French, Hungarian, and Polish. Further translations, languages, and translation how-tos will follow in due time. Locale selection respects the Accept-Language header and can be configured on the account settings page." + +msgid "changelog.2-16.2" +msgstr "Fix distance calculation and visualization of ring line trips. Trips shown in the history map are still partially incorrect." + +msgid "changelog.2-17.1" +msgstr "Map data (“polylines”) for individual trips can now be exported and imported in the GPX format. This allows map data to be corrected or augmented after the fact. The GPX import/export is compatible with BRouter-Web." + +# journey.html.ep + +msgid "journey.not-found" +msgstr "Trip not found." + +#, short +msgid "journey.trip" +msgstr "Trip" + +#, short +msgid "journey.from" +msgstr "From" + +#, short +msgid "journey.to" +msgstr "To" + +#, short +msgid "journey.departure" +msgstr "Departure" + +#, short +msgid "journey.arrival" +msgstr "Arrival" + +#, short +msgid "journey.distance" +msgstr "Distance" + +msgid "journey.beeline.pre" +msgstr "(" + +msgid "journey.beeline.post" +msgstr " as the crow flies)" + +#, short +msgid "journey.speed" +msgstr "Speed" + +#, short +msgid "journey.operator" +msgstr "Operator" + +#, short +msgid "journey.messages" +msgstr "Messages" + +#, short +msgid "journey.comment" +msgstr "Comment" + +#, short +msgid "journey.carriages" +msgstr "Carriages" + +#, short +msgid "journey.route" +msgstr "Route" + +msgid "journey.share" +msgstr "Share" + +msgid "journey.export" +msgstr "Export" + +msgid "journey.edit" +msgstr "Edit" + +msgid "journey.map-data" +msgstr "Map Data" + +msgid "journey.map.download" +msgstr "Download" + +msgid "journey.map.upload" +msgstr "Upload" + +msgid "journey.map.upload-full" +msgstr "Upload full route" + +msgid "journey.map.upload-partial" +msgstr "Upload travelled segment" + +msgid "journey.map.info.download" +msgstr "JSON format: [[lon, lat, station ID], ...], with lon/lat in WGS84 coordinates. GPX files are compatible with BRouter." + +msgid "journey.map.info.upload" +msgstr "GPX uploads must contain a single track with a single track segment (such as provided by BRouter's export). They must cover either the full route or just the travelled route segment that belongs to this checkin. Please use the appropriate upload button, otherwise chaos may ensue. There is no need to specify station IDs when uploading tracks. Note that uploads irreversibly replace previously stored map data." + +msgid "journey.danger" +msgstr "Danger Zone" + +msgid "journey.delete" +msgstr "Delete" + +# landingpage.html.ep + +msgid "landingpage.greeting-prefix" +msgstr "Hello," + +msgid "landingpage.greeting-suffix" +msgstr "!" + +msgid "landingpage.not-checked-in" +msgstr "You are not checked in at the moment" + +msgid "landingpage.stop-geosearch" +msgstr "Look for stops nearby" + +msgid "landingpage.manual-stop-entry" +msgstr "Enter stop manually" + +#, As short as possible +msgid "landingpage.departures" +msgstr "Departures" + +msgid "landingpage.latest-trips" +msgstr "Latest Trips" + +msgid "landingpage.date-format" +msgstr "%b %d %Y" + +msgid "landingpage.about" +msgstr "Travelynx facilitates checkins into public transit (such as buses, trams, or trains) in Germany, Austria, Switzerlanz, Luxembourg, Ireland, Denmark, parts of the USA, and more. This way, you can track your own journeys (often including map and real-time data), share them with others, and examine highly relevant questions such as “how many hours did I spend in public transit in the past month?”." + +msgid "landingpage.traewelling.pre" +msgstr "The idea for such a service was first proposed and implemented by" + +msgid "landingpage.traewelling.post" +msgstr " " + +msgid "landingpage.features" +msgstr "Features:" + +msgid "landingpage.features.log" +msgstr "Log of scheduled and real-time departure and arrival times at departure and destination stop" + +msgid "landingpage.features.share" +msgstr "Sharing the current check-in and past journeys with others" + +msgid "landingpage.features.api-pre" +msgstr "Web-Hooks and an" + +msgid "landingpage.features.api-link" +msgstr "API" + +msgid "landingpage.features.api-post" +msgstr "for automatic check-ins and passing the current status to other applications" + +msgid "landingpage.features.stats" +msgstr "Stats about journey times and delays" + +msgid "landingpage.features.passenger-rights" +msgstr "Support when dealing with passenger rights forms" + +msgid "landingpage.features.public" +msgstr "Optional: public travel status and public data about past journeys" + +msgid "landingpage.disclaimer.lead" +msgstr "Travelynx is a hobby project. It is provided free of charge, without any kind of availability guarantees. Unexpected downtimes or a cancellation of the entire site on short notice are not planned, but always possible." + +msgid "landingpage.disclaimer.source-pre" +msgstr "If you like, you can download the" + +msgid "landingpage.disclaimer.source-link" +msgstr "source code" + +msgid "landingpage.disclaimer.source-post" +msgstr "and host your own instance." + +# language.html.ep + +msgid "language.language" +msgstr "Language" + +msgid "language.browser-default" +msgstr "Use language(s) requested by browser" + +# login.html.ep + +msgid "login.tos" +msgstr "terms of use" + +msgid "login.accept-tos-pre" +msgstr "By logging in, you accept the" + +msgid "login.accept-tos-post" +msgstr " " + +msgid "login.forgot-password" +msgstr "Forgot password" + +msgid "login.registration-disabled" +msgstr "This instance does not allow registration of new accounts at the moment" + +# register.html.ep + +msgid "register.name" +msgstr "Name (alphanumeric)" + +msgid "register.mail" +msgstr "Email address" + +msgid "register.password" +msgstr "Password" + +msgid "register.repeat-password" +msgstr "Repeat password" + +msgid "register.tos" +msgstr "terms of use" + +msgid "register.accept-tos-pre" +msgstr "By submitting this registration form, you accept the" + +msgid "register.accept-tos-post" +msgstr "." + +msgid "register.expect-confirmation-link" +msgstr "After submitting the registration, a confirmation link will be sent to the provided email address. Logging into the new travelynx account is only possible after following that link. The link is valid for 48 hours." + +msgid "register.why-mail" +msgstr "We collect your email address solely fo the purposes of confirming your registration, resetting your password and providing you with important information about your account. Your email address will never be shared with third parties." + +msgid "register.privacy-pre" +msgstr "Our" + +msgid "register.privacy" +msgstr "privacy policy" + +msgid "register.privacy-post" +msgstr "describes additional data that we collect, how we store it and for what purpose." + +msgid "register.account-deletion" +msgstr "Accounts are automatically deleted after a year without activity. You will receive an email informing you about the deletion and giving you four weeks to intervene." + +msgid "register.disclaimer" +msgstr "Please be aware: Travelynx is a non-commerical personal project and is provided as is. We do not make guarantees in regards to availability of the service or a sudden end of the project." + +# _checked_in.html.ep, _public_status_card.html.ep + +msgid "status.is-checked-in" +msgstr "is in transit" + +msgid "status.is-not-checked-in" +msgstr "ist not in transit right now" + +msgid "status.select-destination" +msgstr "Choose destination" + +msgid "status.share" +msgstr "Share" + +msgid "status.check-out" +msgstr "check out" + +#, noun or verb +msgid "status.boarding-in.pre" +msgstr "arrives in" + +#, noun or verb +msgid "status.boarding-in.post" +msgstr " " + +#, noun or verb +msgid "status.boarding-soon" +msgstr "now arriving" + +#, noun or verb +msgid "status.departure-in.pre" +msgstr "departs in" + +#, noun or verb +msgid "status.departure-in.post" +msgstr " " + +#, noun or verb +msgid "status.departure-soon" +msgstr "now departing" + +#, noun or verb +msgid "status.arrival-in.pre" +msgstr "arrives in" + +#, noun or verb +msgid "status.arrival-in.post" +msgstr " " + +#, noun or verb +msgid "status.arrival-soon" +msgstr "now arriving" + +msgid "status.arrival-unknown" +msgstr "Arrival unknown" + +msgid "status.arrived" +msgstr "arrived" + +msgid "status.depart-from.pre" +msgstr "from" + +msgid "status.depart-from.post" +msgstr " " + +msgid "status.arrive-on.pre" +msgstr "on" + +msgid "status.arrive-on.post" +msgstr " " + +msgid "status.delayed-auto-checkout" +msgstr "You will be automatically checked out no less than half an hour after the expected arrival time." + +msgid "status.realtime-unavailable" +msgstr "Real-time data unavailable" + +#, service messages related to the trip +msgid "status.messages" +msgstr "Notifications" + +msgid "status.map" +msgstr "Map" + +msgid "status.change-destination" +msgstr "Change destination" + +msgid "status.carriages" +msgstr "Carriages" + +msgid "status.route" +msgstr "Route" + +#, shown during destination selection +msgid "status.undo" +msgstr "Undo" + +#, shown during destination selection +msgid "status.privacy-level" +msgstr "Privacy" + +#, shown once check-in is completed +msgid "status.undo-checkin" +msgstr "Undo check-in" + +msgid "status.force-checkout-lead" +msgstr "travelynx will perform an automatic check-out no less than half an hour after arrival. In case of backend or data update issues:" + +msgid "status.force-checkout" +msgstr "Force checkout" + +# _history_stats.html.ep + +msgid "stats.trips" +msgstr "Trips" + +msgid "stats.distance" +msgstr "Distance" + +msgid "stats.time" +msgstr "Travel Time" + +msgid "stats.hours" +msgstr "hours" + +msgid "stats.per-schedule" +msgstr "scheduled" + +msgid "stats.total-delay" +msgstr "Total Delay" + +msgid "stats.transfer-time" +msgstr "Transfer Time" + +msgid "stats.on-dep" +msgstr "on departure" + +msgid "stats.on-arr" +msgstr "on arrival" + +# _wagons.html.ep + +msgid "wagons.name-as-type" +msgstr "running as" + +msgid "wagons.from.pre" +msgstr "from" + +msgid "wagons.from.post" +msgstr " " + +msgid "wagons.to.pre" +msgstr "towards" + +msgid "wagons.to.post" +msgstr " " + +msgid "wagons.carriage" +msgstr "Carriage" diff --git a/share/locales/fr_FR.po b/share/locales/fr_FR.po new file mode 100644 index 0000000..f2dcf8f --- /dev/null +++ b/share/locales/fr_FR.po @@ -0,0 +1,253 @@ +msgid "" +msgstr "" +"Language: fr-FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +# +# Global Strings +# + +msgid "strftime.datetime" +msgstr "%d/%m/%Y %Hh%M %Z" + +msgid "button.register" +msgstr "Inscription" + +msgid "button.login" +msgstr "Connexion" + +msgid "button.logout" +msgstr "Déconnexion" + +msgid "footer.imprint" +msgstr "Mentions légales" + +msgid "footer.privacy" +msgstr "Données personnelles" + +msgid "footer.legend" +msgstr "Légende" + +msgid "footer.colour-scheme" +msgstr "Affichage" + +msgid "footer.colour-scheme.light" +msgstr "clair" + +msgid "footer.colour-scheme.dark" +msgstr "sombre" + +msgid "footer.colour-scheme.auto" +msgstr "auto" + +msgid "header.error" +msgstr "erreur" + +# +# Templates +# + +# account.html.ep + +msgid "account.account" +msgstr "Mon compte" + +msgid "account.name" +msgstr "Nom" + +msgid "account.mail" +msgstr "E-mail" + +msgid "account.password" +msgstr "Mot de passe" + +msgid "account.language" +msgstr "Langue" + +msgid "account.connections" +msgstr "Itinéraires préférés" + +msgid "account.connections.enabled" +msgstr "Suggestions activées" + +msgid "account.connections.disabled" +msgstr "Suggestions désactivées" + +msgid "account.visibility" +msgstr "Visibilité" + +msgid "account.interaction" +msgstr "Intéraction" + +msgid "account.interaction.accept-follows" +msgstr "Autoriser à suivre" + +msgid "account.interaction.accept-follow-requests" +msgstr "Autoriser à recevoir une demande de suivi" + +msgid "account.interaction.one" +msgstr "une" + +msgid "account.interaction.open-request" +msgstr "requête en attente" + +msgid "account.interaction.open-requests" +msgstr "requêtes en attente" + +msgid "account.interaction.disabled" +msgstr "Aucun compte ne peut vous suivre" + +# landingpage.html.ep + +msgid "landingpage.greeting-prefix" +msgstr "" + +msgid "landingpage.greeting-suffix" +msgstr "" + +msgid "landingpage.not-checked-in" +msgstr "" + +msgid "landingpage.about" +msgstr "" + +msgid "landingpage.traewelling.pre" +msgstr "" + +msgid "landingpage.traewelling.post" +msgstr "" + +msgid "landingpage.disclaimer.lead" +msgstr "" + +msgid "landingpage.disclaimer.source-pre" +msgstr "" + +msgid "landingpage.disclaimer.source-link" +msgstr "" + +msgid "landingpage.disclaimer.source-post" +msgstr "" + +# language.html.ep + +msgid "language.language" +msgstr "" + +msgid "language.browser-default" +msgstr "" + +# login.html.ep + +msgid "login.tos" +msgstr "conditions d'utilisation" + +msgid "login.accept-tos-pre" +msgstr "En vous connectant, vous acceptez les" + +msgid "login.accept-tos-post" +msgstr " " + +msgid "login.forgot-password" +msgstr "Mot de passe oublié" + +msgid "login.registration-disabled" +msgstr "Cette instance n'accepte pas actuellement de nouvelles inscriptions." + +# register.html.ep + +msgid "register.name" +msgstr "Nom (alphanumeric)" + +msgid "register.mail" +msgstr "Adresse e-mail" + +msgid "register.password" +msgstr "Mot de passe" + +msgid "register.repeat-password" +msgstr "Vérifier le mot de passe" + +msgid "register.tos" +msgstr "conditions d'utilisation" + +msgid "register.accept-tos-pre" +msgstr "En envoyant ce formulaire d'inscription, vous acceptez les" + +msgid "register.accept-tos-post" +msgstr " " + +msgid "register.expect-confirmation-link" +msgstr "Après avoir envoyé votre demande d'inscription, un lien de confirmation sera envoyé à l'adresse e-mail fournie. Vous pourrez vous connecter sur votre compte travelynx après avoir ouvert ce lien. Le lien est valide pendant 48 heures." + +msgid "register.why-mail" +msgstr "" + +msgid "register.privacy-pre" +msgstr "" + +msgid "register.privacy" +msgstr "" + +msgid "register.privacy-post" +msgstr "" + +msgid "register.account-deletion" +msgstr "." + +msgid "register.disclaimer" +msgstr "" + +# _checked_in.html.ep, _public_status_card.html.ep + +msgid "status.is-checked-in" +msgstr "en chemin" + +msgid "status.is-not-checked-in" +msgstr "n'est pas en chemin" + +msgid "status.share" +msgstr "Partager" + +msgid "status.arrival-in.pre" +msgstr "Arrive dans" + +msgid "status.arrival-in.post" +msgstr " " + +msgid "status.arrival-soon" +msgstr "Arrive dans moins d'une minute" + +msgid "status.arrival-unknown" +msgstr "Heure d'arrivée inconnue" + +msgid "status.arrived" +msgstr "Arrivé" + +msgid "status.carriages" +msgstr "Voitures" + +msgid "status.route" +msgstr "Route" + +# _wagons.html.ep + +msgid "wagons.name-as-type" +msgstr "sous le nom de" + +msgid "wagons.from.pre" +msgstr "en provenance de" + +msgid "wagons.from.post" +msgstr " " + +msgid "wagons.to.pre" +msgstr "à destination de" + +msgid "wagons.to.post" +msgstr " " + +msgid "wagons.carriage" +msgstr "Voiture" diff --git a/share/locales/hu_HU.po b/share/locales/hu_HU.po new file mode 100644 index 0000000..7a0c0cb --- /dev/null +++ b/share/locales/hu_HU.po @@ -0,0 +1,549 @@ +msgid "" +msgstr "" +"Language: hu-HU\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +# +# Global Strings +# + +msgid "strftime.datetime" +msgstr "%Y.%m.%d %H:%M %Z" + +msgid "button.register" +msgstr "Regisztrálás" + +msgid "button.login" +msgstr "Bejelentkezés" + +msgid "button.logout" +msgstr "Kijelentkezés" + +msgid "footer.imprint" +msgstr "Impresszum" + +msgid "footer.privacy" +msgstr "Adatvédelem" + +msgid "footer.legend" +msgstr "Jelmagyarázat" + +msgid "footer.colour-scheme" +msgstr "Színséma" + +msgid "footer.colour-scheme.light" +msgstr "világos" + +msgid "footer.colour-scheme.dark" +msgstr "sötét" + +msgid "footer.colour-scheme.auto" +msgstr "automatikus" + +msgid "header.error" +msgstr "Hiba" + +# +# Templates +# + +# about.html.ep + +msgid "about.developed-by.lead" +msgstr "Fejlesztették:" + +msgid "about.developed-by.and" +msgstr "," + +msgid "about.developed-by.others" +msgstr "többiek" + +msgid "about.developed-by.tail" +msgstr " " + +msgid "about.source-code" +msgstr "A Forráskód" + +msgid "about.licence-agplv3" +msgstr "az AGPL v3 licensz alatt elérhető" + +msgid "about.data-sources" +msgstr "Backendek:" + +msgid "about.data-sources.last-and" +msgstr " és" + +msgid "about.disclaimer" +msgstr "Travelynx egy ingyenes és bármi rendelkezésre állási garacia nélküli hobbi projekt. Be nem tervezett leállások, illetve a teljes oldal hírtelen bezárása nincs tervben, de bármikor előfordulhat. Feature requesteket, bug reportokat és egyéb üzeneteket kapacitástól és motivációtól függően azonnal, megkésve vagy akár soha sem lesznek feldolgozva." + +msgid "about.contact" +msgstr "Kapcsolat" + +msgid "about.bugs" +msgstr "Bugok" + +msgid "about.changelog" +msgstr "Changelog" + +# account.html.ep + +msgid "account.changed-name" +msgstr "Név frissítve" + +msgid "account.changed-mail" +msgstr "E-Mail cím frissítve" + +msgid "account.changed-password" +msgstr "Jelszó frissítve" + +msgid "account.changed-language" +msgstr "Nyelv változtatva" + +msgid "account.changed-privacy" +msgstr "Láthatósági beállítások mentve" + +msgid "account.changed-social" +msgstr "Interakciós beállítások mentve" + +msgid "account.changed-traewelling" +msgstr "Träwelling beállítások mentve" + +msgid "account.changed-history" +msgstr "Átszállásos javaslati beállítások mentve" + +msgid "account.changed-webhook" +msgstr "Web Hook frissítve" + +msgid "account.cleared-notifications" +msgstr "Értesítések törölve" + +msgid "account.account" +msgstr "Fiók" + +msgid "account.name" +msgstr "Név" + +msgid "account.mail" +msgstr "E-Mail" + +msgid "account.password" +msgstr "Jelszó" + +msgid "account.language" +msgstr "Nyelv" + +msgid "account.connections" +msgstr "Átszállások" + +msgid "account.connections.enabled" +msgstr "Javaslatok aktiválva" + +msgid "account.connections.disabled" +msgstr "Javaslatok deaktiválva" + +msgid "account.visibility" +msgstr "Láthatóság" + +msgid "account.webhook" +msgstr "Webhook" + +msgid "account.webhook.disabled" +msgstr "Nincs beállitva" + +msgid "account.webhook.active-pending" +msgstr "Aktív, jóváhagyás folyamatban" + +msgid "account.webhook.active-error" +msgstr "Aktív, hibás" + +msgid "account.webhook.active" +msgstr "Aktív" + +msgid "account.traewelling.unsupported" +msgstr "Jelenleg nem támogatott a bahn.de és a transitous közötti inkompatibilitás miatt" + +msgid "account.registration-date" +msgstr "Regisztrálva" + +msgid "account.interaction" +msgstr "Interakció" + +msgid "account.interaction.accept-follows" +msgstr "Bárki követhet" + +msgid "account.interaction.accept-follow-requests" +msgstr "Bárki küldhet követési kéréseket" + +msgid "account.interaction.one" +msgstr "egy" + +msgid "account.interaction.open-request" +msgstr "követési kérés" + +msgid "account.interaction.open-requests" +msgstr "követési kérések" + +msgid "account.interaction.disabled" +msgstr "Senki sem követhet" + +# changelog.html.ep + +msgid "changelog.added" +msgstr "Új fícsör" + +msgid "changelog.bugfix" +msgstr "Bugfix" + +msgid "changelog.2-16.1" +msgstr "Lokalizáció. A travelynx most már angolul, részben pedig franciául, magyarul és lengyelül is elérhető. További fordítások, nyelvek és fordítási útmutatók következnek majd a megfelelő időben. A lokalizáció kiválasztása tiszteletben tartja az Accept-Language fejlécet, és a fiókbeállítások oldalon is beállítható." + +msgid "changelog.2-16.2" +msgstr "A távolságszámítás és a gyűrűsvonali utazások megjelenítése javítva lett. Az előzménytérképen megjelenített utazások még mindig részben hibásak." + +# journey.html.ep + +msgid "journey.not-found" +msgstr "Utazás nem találva." + +#, short +msgid "journey.trip" +msgstr "Utazás" + +#, short +msgid "journey.from" +msgstr "Honnan" + +#, short +msgid "journey.to" +msgstr "Hova" + +#, short +msgid "journey.departure" +msgstr "Indulás" + +#, short +msgid "journey.arrival" +msgstr "Érkezés" + +#, short +msgid "journey.distance" +msgstr "Távolság" + +msgid "journey.beeline.pre" +msgstr "(Légvonalban: " + +msgid "journey.beeline.post" +msgstr ")" + +#, short +msgid "journey.speed" +msgstr "Sebesség" + +#, short +msgid "journey.operator" +msgstr "Üzemeltető" + +#, short +msgid "journey.messages" +msgstr "Jelentések" + +#, short +msgid "journey.comment" +msgstr "Megjegyzés" + +#, short +msgid "journey.carriages" +msgstr "Járművek" + +#, short +msgid "journey.route" +msgstr "Útvonal" + +msgid "journey.share" +msgstr "Megosztás" + +msgid "journey.export" +msgstr "Exportálás" + +msgid "journey.edit" +msgstr "Szerkesztés" + +msgid "journey.delete" +msgstr "Törlés" + +# landingpage.html.ep + +msgid "landingpage.greeting-prefix" +msgstr "Üdv," + +msgid "landingpage.greeting-suffix" +msgstr "!" + +msgid "landingpage.not-checked-in" +msgstr "Jelenleg nem vagy becsekkolva" + +msgid "landingpage.stop-geosearch" +msgstr "Közeledben lévő megállók keresése" + +msgid "landingpage.manual-stop-entry" +msgstr "Megálló manuális bevitele" + +#, As short as possible +msgid "landingpage.departures" +msgstr "Járatok innen" + +msgid "landingpage.latest-trips" +msgstr "Legutóbbi utazások" + +msgid "landingpage.date-format" +msgstr "%Y.%m.%d" + +msgid "landingpage.about" +msgstr "Travelynx-el tömegközlekedési eszközökre (pl. Buszokra, Villamosokra, Vonatokra) be lehet csekkolni, többek között Németországban, Ausztriában, Svájcban, Luxemburgban, Írországban, Dániában és az USA egyes részeiben. Így nyomon követheted az utazásaidat (gyakran térképes és valós idejű adatokkal együtt), megoszthatod azokat másokkal, és a rendkívüli fontos kérdéseidre, mint például „hány órát tömegközlekedtem az elmúlt hónapban?”, is kapsz választ." + +msgid "landingpage.traewelling.pre" +msgstr "Az elsők akiknek hasonló ötlete volt és azt kivitelezték a" + +msgid "landingpage.traewelling.post" +msgstr " volt" + +msgid "landingpage.features" +msgstr "Fícsörök:" # not sure if this or rather "Feature-ök" I've seen both be used before. + +msgid "landingpage.features.log" +msgstr "Menetrend szerinti és valós idejű adatok naplózása. (indulási és célállomásokon)" + +msgid "landingpage.features.share" +msgstr "Jelenlegi és korábbi utazások megosztása másokkal" + +msgid "landingpage.features.api-pre" +msgstr "Web-Hook-ok és egy" + +msgid "landingpage.features.api-link" +msgstr "API" + +msgid "landingpage.features.api-post" +msgstr "az automatikus becsekkeléshez és az aktuális státusz leolvasásához" + +msgid "landingpage.features.stats" +msgstr "Statisztikák az utazások időtartamáról és a késésekről" + +msgid "landingpage.features.passenger-rights" +msgstr "Támogatás az utasjogi formanyomtatványok kitöltéséhez" + +msgid "landingpage.features.public" +msgstr "Opcionális: utazási státusz és a korábbi utazásokról szóló adatok nyilvánosítása" + +msgid "landingpage.disclaimer.lead" +msgstr "Travelynx egy ingyenes és bármi rendelkezésre állási garacia nélküli hobbi projekt. Be nem tervezett leállások, illetve a teljes oldal hírtelen bezárása nincs tervben, de bármikor előfordulhat." + +msgid "landingpage.disclaimer.source-pre" +msgstr "Aki szeretné az letöltheti a" + +msgid "landingpage.disclaimer.source-link" +msgstr "forráskódot" + +msgid "landingpage.disclaimer.source-post" +msgstr "is és saját szervert működtethet." + +# language.html.ep + +msgid "language.language" +msgstr "Nyelv" + +msgid "language.browser-default" +msgstr "A webböngésző nyelve(i)" + +# login.html.ep + +msgid "login.accept-tos-pre" +msgstr "A bejelentkezéssel elfogadod a" + +msgid "login.tos" +msgstr "használati feltételeket" + +msgid "login.accept-tos-post" +msgstr " " + +msgid "login.forgot-password" +msgstr "Elfelejtett jelszó" + +msgid "login.registration-disabled" +msgstr "Ez a weboldal jelenleg nem fogad új felhasználókat" + +# register.html.ep + +msgid "register.name" +msgstr "Név (alfanumerikus)" + +msgid "register.mail" +msgstr "E-Mail cím" + +msgid "register.password" +msgstr "Jelszó" + +msgid "register.repeat-password" +msgstr "Jelszó újra" + +msgid "register.accept-tos-pre" +msgstr "A regisztrálással elfogadod a" + +msgid "register.tos" +msgstr "használati feltételeket" + +msgid "register.accept-tos-post" +msgstr " " + +msgid "register.expect-confirmation-link" +msgstr "A regisztráció elküldése után egy megerősítő linket küldünk a megadott e-mail címre. Az új travelynx-fiókba való bejelentkezés csak a link követése után lehetséges. A link 48 órán át érvényes." + +msgid "register.why-mail" +msgstr "Az e-mail címet csak a regisztráció megerősítésére, az „Elfelejtett jelszó” funkcióhoz és a fiókkal kapcsolatos fontos információkhoz használjuk, és nem adjuk tovább harmadik félnek." + +msgid "register.privacy-pre" +msgstr "Az" + +msgid "register.privacy" +msgstr "adatkezelési tájékoztató" + +msgid "register.privacy-post" +msgstr "leírja az egyéb begyűjtött adatokat, valamit azok célját és tárolási időtartamát." + +msgid "register.account-deletion" +msgstr "A fiókokat egy év inaktivitás után e-mailben értesítjük a közelgő törlésről, és további négy hét inaktivitás után automatikusan törlődnek." + +msgid "register.disclaimer" +msgstr "Kérjük, vedd figyelembe: Travelynx egy ingyenes és bármi rendelkezésre állási garacia nélküli hobbi projekt. Be nem tervezett leállások, illetve a teljes oldal hírtelen bezárása nincs tervben, de bármikor előfordulhat." + +# _checked_in.html.ep, _public_status_card.html.ep + +msgid "status.is-checked-in" +msgstr "útban van" + +msgid "status.is-not-checked-in" +msgstr "jelenleg nincs becsekkolva" + +msgid "status.select-destination" +msgstr "Cél kiválasztása" + +msgid "status.share" +msgstr "Megosztás" + +msgid "status.check-out" +msgstr "Kicsekkelés" + +#, noun or verb +msgid "status.boarding-in.pre" +msgstr " " + +#, noun or verb +msgid "status.boarding-in.post" +msgstr "-en belül érkezik" + +#, noun or verb +msgid "status.boarding-soon" +msgstr "A beszállás hamarosan következik" + +#, noun or verb +msgid "status.departure-in.pre" +msgstr " " + +#, noun or verb +msgid "status.departure-in.post" +msgstr "-en belül indul" + +#, noun or verb +msgid "status.departure-soon" +msgstr "Egy percen belül indul" + +#, noun or verb +msgid "status.arrival-in.pre" +msgstr " " + +#, noun or verb +msgid "status.arrival-in.post" +msgstr "-en belül érkezik" + +#, noun or verb +msgid "status.arrival-soon" +msgstr "Egy percen belül érkezik" + +msgid "status.arrival-unknown" +msgstr "Érkezési időpont ismeretlen" + +msgid "status.arrived" +msgstr "Cél elérve" + +msgid "status.depart-from.pre" +msgstr " " + +msgid "status.depart-from.post" +msgstr "vágányról indul" + +msgid "status.arrive-on.pre" +msgstr " " + +msgid "status.arrive-on.post" +msgstr "vágányra érkezik" + +msgid "status.delayed-auto-checkout" +msgstr "Körülbelül tíz perccel az érkezés után automatikusan kicsekkolódsz." + +msgid "status.realtime-unavailable" +msgstr "Valós idejű adatok nem állnak rendelkezésre" + +#, service messages related to the trip +msgid "status.messages" +msgstr "Jelentések" + +msgid "status.map" +msgstr "Térkép" + +msgid "status.change-destination" +msgstr "Célváltás?" + +msgid "status.carriages" +msgstr "Kocsik" + +msgid "status.route" +msgstr "Útvonal" + +#, shown during destination selection +msgid "status.undo" +msgstr "Visszavonás" + +#, shown during destination selection +msgid "status.privacy-level" +msgstr "Láthatóság" + +#, shown once check-in is completed +msgid "status.undo-checkin" +msgstr "Becsekkelés visszavonása" + +msgid "status.force-checkout-lead" +msgstr "Körülbelül tíz perccel az érkezés után automatikusan kicsekkolódsz. Ha a backend leállt, vagy az utazás más okokból elveszlett:" + +msgid "status.force-checkout" +msgstr "Most kicsekkelni" + +# _wagons.html.ep + +msgid "wagons.name-as-type" +msgstr "közlekedik mint:" + +msgid "wagons.from.pre" +msgstr " " + +msgid "wagons.from.post" +msgstr "-tól" + +msgid "wagons.to.pre" +msgstr " " + +msgid "wagons.to.post" +msgstr "-ig" + +msgid "wagons.carriage" +msgstr "Kocsi" diff --git a/share/locales/pl_PL.po b/share/locales/pl_PL.po new file mode 100644 index 0000000..0d9cc63 --- /dev/null +++ b/share/locales/pl_PL.po @@ -0,0 +1,623 @@ +msgid "" +msgstr "" +"Language: pl-PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +# +# Global Strings +# + +msgid "strftime.datetime" +msgstr "%d.%m.%Y %H:%M %Z" + +msgid "button.register" +msgstr "Rejestracja" + +msgid "button.login" +msgstr "Logowanie" + +msgid "button.logout" +msgstr "Wyloguj się" + +msgid "footer.imprint" +msgstr "Imprint" + +msgid "footer.privacy" +msgstr "Prywatność" + +msgid "footer.legend" +msgstr "Legenda" + +msgid "footer.colour-scheme" +msgstr "Tryb wyświetlania" + +msgid "footer.colour-scheme.light" +msgstr "jasny" + +msgid "footer.colour-scheme.dark" +msgstr "ciemny" + +msgid "footer.colour-scheme.auto" +msgstr "automatyczny" + +msgid "header.error" +msgstr "Błąd" + +# +# Templates +# + +# about.html.ep + +msgid "about.developed-by.lead" +msgstr "Stworzone przez" + +msgid "about.developed-by.and" +msgstr "i" + +msgid "about.developed-by.others" +msgstr "innych" + +msgid "about.developed-by.tail" +msgstr " " + +msgid "about.source-code" +msgstr "Kod źródłowy" + +msgid "about.licence-agplv3" +msgstr "objęty licencją AGPL v3" + +msgid "about.data-sources" +msgstr "Backends" + +msgid "about.data-sources.last-and" +msgstr " i" + +msgid "about.disclaimer" +msgstr "Travelynx to bezpłatny, prywatnie prowadzony projekt, który nie gwarantuje dostępności. Nieplanowane przerwy w działaniu lub nagłe zakończenie działania strony nie są przewidziane, ale mogą się zdarzyć. Prośby o nowe funkcje, zgłoszenia błędów oraz inne wiadomości są rozpatrywane i odpowiadane w zależności od dostępnych zasobów i motywacji — szybko, z opóźnieniem lub wcale." + +msgid "about.contact" +msgstr "Kontakt" + +msgid "about.bugs" +msgstr "Błędy" + +msgid "about.changelog" +msgstr "Zmiany" + +# account.html.ep + +msgid "account.changed-name" +msgstr "Nazwa została zmieniona" + +msgid "account.changed-mail" +msgstr "Adres e-mail został zmieniony" + +msgid "account.changed-password" +msgstr "Hasło zostało zmienione" + +msgid "account.changed-language" +msgstr "Język został zmieniony" + +msgid "account.changed-privacy" +msgstr "Zmieniono ustawienia dotyczące publicznych danych konta" + +msgid "account.changed-social" +msgstr "Zmieniono ustawienia dotyczące interakcji z innymi kontami" + +msgid "account.changed-traewelling" +msgstr "Zaktualizowano powiązanie z Träwelling" + +msgid "account.changed-history" +msgstr "Zmieniono ustawienia dotyczące proponowanych połączeń" + +msgid "account.changed-webhook" +msgstr "Zaktualizowano Webhook" + +msgid "account.cleared-notifications" +msgstr "Powiadomienia zostały przeczytane" + +msgid "account.account" +msgstr "Konto" + +msgid "account.name" +msgstr "Nazwa" + +msgid "account.mail" +msgstr "Adres e-mail" + +msgid "account.password" +msgstr "Hasło" + +msgid "account.language" +msgstr "Język" + +msgid "account.connections" +msgstr "Połączenia" + +msgid "account.connections.enabled" +msgstr "Sugestie włączone" + +msgid "account.connections.disabled" +msgstr "Sugestie wyłączone" + +msgid "account.visibility" +msgstr "Widoczność" + +msgid "account.webhook" +msgstr "Webhook" + +msgid "account.webhook.disabled" +msgstr "Nie skonfigurowano" + +msgid "account.webhook.active-pending" +msgstr "Aktywny, jeszcze nie wykonany" + +msgid "account.webhook.active-error" +msgstr "Aktywny, błąd wykonania" + +msgid "account.webhook.active" +msgstr "Aktywny" + +msgid "account.traewelling.unsupported" +msgstr "Obecnie nieobsługiwane z powodu niekompatybilności między bahn.de a transitous" + +msgid "account.registration-date" +msgstr "Zarejestrowano dnia" + +msgid "account.interaction" +msgstr "Interakcje" + +msgid "account.interaction.accept-follows" +msgstr "Inne konta mogą cię obserwować" + +msgid "account.interaction.accept-follow-requests" +msgstr "Inne konta mogą wysyłać ci prośby o obserwowanie" + +msgid "account.interaction.one" +msgstr "jedna" + +msgid "account.interaction.open-request" +msgstr "otwarta prośba" + +msgid "account.interaction.open-requests" +msgstr "otwarte prośby" + +msgid "account.interaction.disabled" +msgstr "Inne konta nie mogą cię obserwować" + +msgid "account.profile" +msgstr "Profil publiczny" + +msgid "account.interaction.requests" +msgstr "Otwarte zapytania" + +msgid "account.interaction.requests.incoming.none" +msgstr "brak przychodzących" + +msgid "account.interaction.requests.incoming.some" +msgstr "przychodzące" + +msgid "account.interaction.requests.outgoing.none" +msgstr "brak wychodzących" + +msgid "account.interaction.requests.outgoing.some" +msgstr "wychodzące" + +# changelog.html.ep + +msgid "changelog.added" +msgstr "Nowa funkcja" + +msgid "changelog.bugfix" +msgstr "Poprawka błędu" + +msgid "changelog.2-16.1" +msgstr "Infrastruktura do tłumaczenia Travelynx na inne języki. Większość strony nadal jest po niemiecku; pojedyncze strony są już dostępne po angielsku, a ich częściowo także po francusku, polsku i węgiersku. Kolejne tłumaczenia oraz ewentualne nowe języki i dokumentacja wspierająca ten proces pojawią się z czasem. Język jest wybierany na podstawie nagłówka Accept-Language i może być dodatkowo skonfigurowany w ustawieniach konta." + +msgid "changelog.2-16.2" +msgstr "Poprawne obliczanie i wizualizacja trasy przejazdu dla linii okrężnych. Wyświetlanie przejazdów na mapie podróży nadal zawiera częściowe błędy." + +msgid "changelog.2-17.1" +msgstr "Dane mapy („Polylines”) dla pojedynczych przejazdów można teraz eksportować i importować jako pliki GPX. Dzięki temu dane mapy można później poprawić lub uzupełnić. Format danych GPX jest kompatybilny z BRouter-Web." + +# journey.html.ep + +msgid "journey.not-found" +msgstr "Przejazd nie został znaleziony." + +#, short +msgid "journey.trip" +msgstr "Przejazd" + +#, short +msgid "journey.from" +msgstr "Z" + +#, short +msgid "journey.to" +msgstr "Do" + +#, short +msgid "journey.departure" +msgstr "Odjazd" + +#, short +msgid "journey.arrival" +msgstr "Przyjazd" + +#, short +msgid "journey.distance" +msgstr "Dystans" + +msgid "journey.beeline.pre" +msgstr "(Linia prosta: " + +msgid "journey.beeline.post" +msgstr ")" + +#, short +msgid "journey.speed" +msgstr "Prędkość" + +#, short +msgid "journey.operator" +msgstr "Operator" + +#, short +msgid "journey.messages" +msgstr "Komunikaty" + +#, short +msgid "journey.comment" +msgstr "Komentarz" + +#, short +msgid "journey.carriages" +msgstr "Skład pociągu" + +#, short +msgid "journey.route" +msgstr "Trasa" + +msgid "journey.share" +msgstr "Udostępnij" + +msgid "journey.export" +msgstr "Eksportuj" + +msgid "journey.edit" +msgstr "Edytuj" + +msgid "journey.map-data" +msgstr "Dane mapy" + +msgid "journey.map.download" +msgstr "Pobierz" + +msgid "journey.map.upload" +msgstr "Prześlij" + +msgid "journey.map.upload-full" +msgstr "Cała trasa" + +msgid "journey.map.upload-partial" +msgstr "Przejechany odcinek" + +msgid "journey.map.info.download" +msgstr "Format JSON: [[lon, lat, ID], ...] w współrzędnych WGS84. Pliki GPX są kompatybilne z BRouter." + +msgid "journey.map.info.upload" +msgstr "Pliki GPX muszą zawierać jeden element track z jednym segmentem track. Eksport GPX z BRouter spełnia te wymagania. Przesyłane dane muszą zawierać całą trasę środka transportu lub tylko odcinek odpowiadający temu checkinowi. Przy przesyłaniu wybierz odpowiedni przycisk. ID przystanków nie muszą być podane. Uwaga: przesłanie własnych danych mapy bezpowrotnie usuwa wcześniej zapisane dane." + +msgid "journey.danger" +msgstr "Danger Zone" + +msgid "journey.delete" +msgstr "Usuń" + +# landingpage.html.ep + +msgid "landingpage.greeting-prefix" +msgstr "Cześć," + +msgid "landingpage.greeting-suffix" +msgstr "!" + +msgid "landingpage.not-checked-in" +msgstr "Nie jesteś obecnie zameldowany." + +msgid "landingpage.stop-geosearch" +msgstr "Szukaj stacji w pobliżu" + +msgid "landingpage.manual-stop-entry" +msgstr "Wprowadź ręcznie" + +#, As short as possible +msgid "landingpage.departures" +msgstr "Odjazdy" + +msgid "landingpage.latest-trips" +msgstr "Ostatnie przejazdy" + +msgid "landingpage.date-format" +msgstr "%d.%m.%Y" + +msgid "landingpage.about" +msgstr "Travelynx umożliwia zameldowanie się w środkach transportu (autobusach, tramwajach, pociągach) między innymi w Niemczech, Austrii, Szwajcarii, Luksemburgu, Irlandii, Danii i niektórych częściach Stanów Zjednoczonych. Dzięki temu można później śledzić własne podróże wraz z danymi w czasie rzeczywistym i komunikatami serwisowymi oraz uzyskać odpowiedzi na palące pytania, takie jak „Ile godzin spędziłem w podróży w zeszłym miesiącu?”." + +msgid "landingpage.traewelling.pre" +msgstr "Pomysł pochodzi od" + +msgid "landingpage.traewelling.post" +msgstr " " + +msgid "landingpage.features" +msgstr "Funkcje:" + +msgid "landingpage.features.log" +msgstr "Protokół danych rozkładu jazdy i danych w czasie rzeczywistym na stacji początkowej i końcowej" + +msgid "landingpage.features.share" +msgstr "Udostępnianie aktualnych i przeszłych przejazdów innym osobom" + +msgid "landingpage.features.api-pre" +msgstr "Webhooki oraz" + +msgid "landingpage.features.api-link" +msgstr "API" + +msgid "landingpage.features.api-post" +msgstr "do automatycznego zameldowania się i odczytu aktualnego statusu" + +msgid "landingpage.features.stats" +msgstr "Statystyki dotyczące czasu podróży i opóźnień" + +msgid "landingpage.features.passenger-rights" +msgstr "Wsparcie przy wypełnianiu formularzy praw pasażera" + +msgid "landingpage.features.public" +msgstr "Opcjonalnie: publiczny status podróży i publiczne dane o przeszłych przejazdach" + +msgid "landingpage.disclaimer.lead" +msgstr "Travelynx jest bezpłatnym, prywatnym projektem bez gwarancji dostępności. Nieprzewidziane przerwy w działaniu lub krótkoterminowe wyłączenie tej strony nie są planowane, ale są możliwe." + +msgid "landingpage.disclaimer.source-pre" +msgstr "Kto chce, może również pobrać" + +msgid "landingpage.disclaimer.source-link" +msgstr "kod źródłowy" + +msgid "landingpage.disclaimer.source-post" +msgstr "i skonfigurować własną instancję." + +# language.html.ep + +msgid "language.language" +msgstr "Język" + +msgid "language.browser-default" +msgstr "Ten sam język co przeglądarka internetowa" + +# login.html.ep + +msgid "login.tos" +msgstr "zasady użytkowania" + +msgid "login.accept-tos-pre" +msgstr "Logując się akceptujesz" + +msgid "login.accept-tos-post" +msgstr " " + +msgid "login.forgot-password" +msgstr "Nie pamiętam hasła" + +msgid "login.registration-disabled" +msgstr "Ta instancja nie zezwala w tej chwili na nowe rejestracje" + +# register.html.ep + +msgid "register.name" +msgstr "Nazwa (alfanumeryczna)" + +msgid "register.mail" +msgstr "Adres e-mail" + +msgid "register.password" +msgstr "Hasło" + +msgid "register.repeat-password" +msgstr "Powtórz hasło" + +msgid "register.tos" +msgstr "zasady użytkowania" + +msgid "register.accept-tos-pre" +msgstr "Rejestrując się akceptujesz" + +msgid "register.accept-tos-post" +msgstr " " + +msgid "register.expect-confirmation-link" +msgstr "Po przesłaniu rejestracji na podany adres e-mail zostanie wysłany link potwierdzający. Zalogowanie się do konta travelynx jest możliwe dopiero po kliknięciu tego linku. Link jest ważny przez 48 godzin." + +msgid "register.why-mail" +msgstr "Adres e-mail będzie wykorzystywany wyłącznie do potwierdzenia rejestracji, funkcji „Zapomniałem hasła” oraz ważnych informacji dotyczących konta i nie będzie udostępniany osobom trzecim." + +msgid "register.privacy-pre" +msgstr "" + +msgid "register.privacy" +msgstr "Polityka prywatności" + +msgid "register.privacy-post" +msgstr "opisuje inne gromadzone dane, a także ich cel i okresy przechowywania." + +msgid "register.account-deletion" +msgstr "Po roku braku aktywności użytkownicy otrzymują wiadomość e-mail z informacją o zbliżającym się usunięciu konta, a po kolejnych czterech tygodniach braku aktywności konta są automatycznie usuwane." + +msgid "register.disclaimer" +msgstr "Uwaga: Travelynx jest prywatnym projektem bez gwarancji dostępności. Niezapowiedziane przerwy w działaniu lub krótkoterminowe wyłączenie tej strony nie są planowane, ale są możliwe." + +# _checked_in.html.ep, _public_status_card.html.ep + +msgid "status.is-checked-in" +msgstr "jest w podróży" + +msgid "status.is-not-checked-in" +msgstr "nie jest w tej chwili w podróży" + +msgid "status.select-destination" +msgstr "Wybierz cel" + +msgid "status.share" +msgstr "Udostępnij" + +msgid "status.check-out" +msgstr "Wymelduj się" + +#, noun or verb +msgid "status.boarding-in.pre" +msgstr "Wjazd za" + +#, noun or verb +msgid "status.boarding-in.post" +msgstr " " + +#, noun or verb +msgid "status.boarding-soon" +msgstr "wjeżdża" + +#, noun or verb +msgid "status.departure-in.pre" +msgstr "Odjazd za" + +#, noun or verb +msgid "status.departure-in.post" +msgstr " " + +#, noun or verb +msgid "status.departure-soon" +msgstr "odjeżdża" + +#, noun or verb +msgid "status.arrival-in.pre" +msgstr "Dojazd za" + +#, noun or verb +msgid "status.arrival-in.post" +msgstr " " + +#, noun or verb +msgid "status.arrival-soon" +msgstr "Dojazd za mniej niż minutę" + +msgid "status.arrival-unknown" +msgstr "Czas dojazdu nieznany" + +msgid "status.arrived" +msgstr "Cel osiągnięty" + +msgid "status.depart-from.pre" +msgstr "z" + +msgid "status.depart-from.post" +msgstr " " + +msgid "status.arrive-on.pre" +msgstr "na" + +msgid "status.arrive-on.post" +msgstr " " + +msgid "status.delayed-auto-checkout" +msgstr "Automatyczne wymeldowanie nastąpi najpóźniej pół godziny po przyjeździe." + +msgid "status.realtime-unavailable" +msgstr "Brak danych w czasie rzeczywistym" + +#, service messages related to the trip +msgid "status.messages" +msgstr "Komunikaty" + +msgid "status.map" +msgstr "Mapa" + +msgid "status.change-destination" +msgstr "Zmień cel?" + +msgid "status.carriages" +msgstr "Wagony" + +msgid "status.route" +msgstr "Trasa" + +#, shown during destination selection +msgid "status.undo" +msgstr "Cofnij" + +#, shown during destination selection +msgid "status.privacy-level" +msgstr "Widoczność" + +#, shown once check-in is completed +msgid "status.undo-checkin" +msgstr "Cofnij meldowanie" + +msgid "status.force-checkout-lead" +msgstr "Automatyczne wymeldowanie nastąpi najpóźniej pół godziny po przyjeździe. Jeśli backend nie działa lub przejazd został utracony z innych powodów:" + +msgid "status.force-checkout" +msgstr "Wymelduj się teraz" + +# _history_stats.html.ep + +msgid "stats.trips" +msgstr "Ilość przejazdów" + +msgid "stats.distance" +msgstr "Dystans" + +msgid "stats.time" +msgstr "Czas przejazdów" + +msgid "stats.hours" +msgstr "godzin" + +msgid "stats.per-schedule" +msgstr "Zgodnie z rozkładem" + +msgid "stats.transfer-time" +msgstr "Czas oczekiwania (przesiadki)" + +msgid "stats.total-delay" +msgstr "Skumulowane opóźnienie" + +msgid "stats.on-dep" +msgstr "Przy odjeździe" + +msgid "stats.on-arr" +msgstr "Przy przyjeździe" + +# _wagons.html.ep + +msgid "wagons.name-as-type" +msgstr "jedzie jako" + +msgid "wagons.from.pre" +msgstr "z" + +msgid "wagons.from.post" +msgstr " " + +msgid "wagons.to.pre" +msgstr "do" + +msgid "wagons.to.post" +msgstr " " + +msgid "wagons.carriage" +msgstr "Wagon" diff --git a/share/locales/reference.md b/share/locales/reference.md new file mode 100644 index 0000000..13f51b1 --- /dev/null +++ b/share/locales/reference.md @@ -0,0 +1,1609 @@ +# Translation Status + +* de-DE: 100.0% complete (0 missing) +* en-GB: 100.0% complete (0 missing) +* fr-FR: 30.9% complete (129 missing) +* hu-HU: 86.7% complete (25 missing) +* pl-PL: 100.0% complete (0 missing) + +### + +* de-DE: Language: de-DE +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Plural-Forms: nplurals=2; plural=n != 1; + +* en-GB: Language: en-GB +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Plural-Forms: nplurals=2; plural=n != 1; + +* fr-FR: Language: fr-FR +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Plural-Forms: nplurals=2; plural=(n > 1); + +* hu-HU: Language: hu-HU +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Plural-Forms: nplurals=2; plural=(n > 1); + +* pl-PL: Language: pl-PL +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 && (n<10 || n>=20) ? 1 : 2); + + +## Global Strings + +### strftime.datetime + +* de-DE: %d.%m.%Y %H:%M %Z +* en-GB: %Y-%m-%d %H:%M %Z +* fr-FR: %d/%m/%Y %Hh%M %Z +* hu-HU: %Y.%m.%d %H:%M %Z +* pl-PL: %d.%m.%Y %H:%M %Z + +### button.register + +* de-DE: Registrieren +* en-GB: Register +* fr-FR: Inscription +* hu-HU: Regisztrálás +* pl-PL: Rejestracja + +### button.login + +* de-DE: Anmelden +* en-GB: Login +* fr-FR: Connexion +* hu-HU: Bejelentkezés +* pl-PL: Logowanie + +### button.logout + +* de-DE: Abmelden +* en-GB: Logout +* fr-FR: Déconnexion +* hu-HU: Kijelentkezés +* pl-PL: Wyloguj się + +### footer.imprint + +* de-DE: Impressum +* en-GB: Imprint +* fr-FR: Mentions légales +* hu-HU: Impresszum +* pl-PL: Imprint + +### footer.privacy + +* de-DE: Datenschutz +* en-GB: Privacy +* fr-FR: Données personnelles +* hu-HU: Adatvédelem +* pl-PL: Prywatność + +### footer.legend + +* de-DE: Legende +* en-GB: Legend +* fr-FR: Légende +* hu-HU: Jelmagyarázat +* pl-PL: Legenda + +### footer.colour-scheme + +* de-DE: Farbschema +* en-GB: Display Mode +* fr-FR: Affichage +* hu-HU: Színséma +* pl-PL: Tryb wyświetlania + +### footer.colour-scheme.light + +* de-DE: hell +* en-GB: light +* fr-FR: clair +* hu-HU: világos +* pl-PL: jasny + +### footer.colour-scheme.dark + +* de-DE: dunkel +* en-GB: dark +* fr-FR: sombre +* hu-HU: sötét +* pl-PL: ciemny + +### footer.colour-scheme.auto + +* de-DE: automatisch +* en-GB: auto +* fr-FR: auto +* hu-HU: automatikus +* pl-PL: automatyczny + +### header.error + +* de-DE: Fehler +* en-GB: Error +* fr-FR: Erreur +* hu-HU: Hiba +* pl-PL: Błąd + +## Templates + +## about.html.ep + +### about.developed-by.lead + +* de-DE: Entwickelt von +* en-GB: Developed by +* fr-FR *missing* +* hu-HU: Fejlesztették: +* pl-PL: Stworzone przez + +### about.developed-by.and + +* de-DE: und +* en-GB: and +* fr-FR *missing* +* hu-HU: , +* pl-PL: i + +### about.developed-by.others + +* de-DE: weiteren +* en-GB: others +* fr-FR *missing* +* hu-HU: többiek +* pl-PL: innych + +### about.developed-by.tail + +* de-DE: +* en-GB: +* fr-FR *missing* +* hu-HU: +* pl-PL: + +### about.source-code + +* de-DE: Quelltext +* en-GB: Source code +* fr-FR *missing* +* hu-HU: A Forráskód +* pl-PL: Kod źródłowy + +### about.licence-agplv3 + +* de-DE: lizensiert unter AGPL v3 +* en-GB: available under the terms of AGPL v3 +* fr-FR *missing* +* hu-HU: az AGPL v3 licensz alatt elérhető +* pl-PL: objęty licencją AGPL v3 + +### about.data-sources + +* de-DE: Backends +* en-GB: Backends +* fr-FR *missing* +* hu-HU: Backendek: +* pl-PL: Backends + +### about.data-sources.last-and + +* de-DE: und +* en-GB: , and +* fr-FR *missing* +* hu-HU: és +* pl-PL: i + +### about.disclaimer + +* de-DE: 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. +* en-GB: Travelynx is a hobby project. It is provided free of charge, without any kind of availability guarantees. Unexpected downtimes or a cancellation of the entire site on short notice are not planned, but always possible. Depending on available spare time and motivation, feature requests, bug reports, and other messages are processed promptly, with delay, or not at all. +* fr-FR *missing* +* hu-HU: Travelynx egy ingyenes és bármi rendelkezésre állási garacia nélküli hobbi projekt. Be nem tervezett leállások, illetve a teljes oldal hírtelen bezárása nincs tervben, de bármikor előfordulhat. Feature requesteket, bug reportokat és egyéb üzeneteket kapacitástól és motivációtól függően azonnal, megkésve vagy akár soha sem lesznek feldolgozva. +* pl-PL: Travelynx to bezpłatny, prywatnie prowadzony projekt, który nie gwarantuje dostępności. Nieplanowane przerwy w działaniu lub nagłe zakończenie działania strony nie są przewidziane, ale mogą się zdarzyć. Prośby o nowe funkcje, zgłoszenia błędów oraz inne wiadomości są rozpatrywane i odpowiadane w zależności od dostępnych zasobów i motywacji — szybko, z opóźnieniem lub wcale. + +### about.contact + +* de-DE: Kontakt +* en-GB: Contact +* fr-FR *missing* +* hu-HU: Kapcsolat +* pl-PL: Kontakt + +### about.bugs + +* de-DE: Bugs +* en-GB: Bugs +* fr-FR *missing* +* hu-HU: Bugok +* pl-PL: Błędy + +### about.changelog + +* de-DE: Änderungen +* en-GB: Changelog +* fr-FR *missing* +* hu-HU: Changelog +* pl-PL: Zmiany + +## account.html.ep + +### account.changed-name + +* de-DE: Name geändert +* en-GB: Updated name +* fr-FR *missing* +* hu-HU: Név frissítve +* pl-PL: Nazwa została zmieniona + +### account.changed-mail + +* de-DE: Mail-Adresse geändert +* en-GB: Updated email address +* fr-FR *missing* +* hu-HU: E-Mail cím frissítve +* pl-PL: Adres e-mail został zmieniony + +### account.changed-password + +* de-DE: Passwort geändert +* en-GB: Updated password +* fr-FR *missing* +* hu-HU: Jelszó frissítve +* pl-PL: Hasło zostało zmienione + +### account.changed-language + +* de-DE: Sprache geändert +* en-GB: Changed language +* fr-FR *missing* +* hu-HU: Nyelv változtatva +* pl-PL: Język został zmieniony + +### account.changed-privacy + +* de-DE: Einstellungen zu öffentlichen Account-Daten geändert +* en-GB: Privacy settings have been saved +* fr-FR *missing* +* hu-HU: Láthatósági beállítások mentve +* pl-PL: Zmieniono ustawienia dotyczące publicznych danych konta + +### account.changed-social + +* de-DE: Einstellungen zur Interaktionen mit anderen Accounts geändert +* en-GB: Social settings have been saved +* fr-FR *missing* +* hu-HU: Interakciós beállítások mentve +* pl-PL: Zmieniono ustawienia dotyczące interakcji z innymi kontami + +### account.changed-traewelling + +* de-DE: Träwelling-Verknüpfung aktualisiert +* en-GB: Träwelling settings have been saved +* fr-FR *missing* +* hu-HU: Träwelling beállítások mentve +* pl-PL: Zaktualizowano powiązanie z Träwelling + +### account.changed-history + +* de-DE: Einstellungen zu vorgeschlagenen Verbindungen geändert +* en-GB: Connection suggestion settings have been saved +* fr-FR *missing* +* hu-HU: Átszállásos javaslati beállítások mentve +* pl-PL: Zmieniono ustawienia dotyczące proponowanych połączeń + +### account.changed-webhook + +* de-DE: Web Hook aktualisiert +* en-GB: Web Hook has been updated +* fr-FR *missing* +* hu-HU: Web Hook frissítve +* pl-PL: Zaktualizowano Webhook + +### account.cleared-notifications + +* de-DE: Benachrichtigungen gelesen +* en-GB: Notifications have been cleared +* fr-FR *missing* +* hu-HU: Értesítések törölve +* pl-PL: Powiadomienia zostały przeczytane + +### account.account + +* de-DE: Account +* en-GB: Account +* fr-FR: Mon compte +* hu-HU: Fiók +* pl-PL: Konto + +### account.name + +* de-DE: Name +* en-GB: Name +* fr-FR: Nom +* hu-HU: Név +* pl-PL: Nazwa + +### account.mail + +* de-DE: E-Mail +* en-GB: E-Mail +* fr-FR: E-mail +* hu-HU: E-Mail +* pl-PL: Adres e-mail + +### account.password + +* de-DE: Passwort +* en-GB: Password +* fr-FR: Mot de passe +* hu-HU: Jelszó +* pl-PL: Hasło + +### account.language + +* de-DE: Sprache +* en-GB: Language +* fr-FR: Langue +* hu-HU: Nyelv +* pl-PL: Język + +### account.connections + +* de-DE: Verbindungen +* en-GB: Connections +* fr-FR: Itinéraires préférés +* hu-HU: Átszállások +* pl-PL: Połączenia + +### account.connections.enabled + +* de-DE: Vorschläge aktiv +* en-GB: Suggestions enabled +* fr-FR: Suggestions activées +* hu-HU: Javaslatok aktiválva +* pl-PL: Sugestie włączone + +### account.connections.disabled + +* de-DE: Vorschläge deaktiviert +* en-GB: Suggestions disabled +* fr-FR: Suggestions désactivées +* hu-HU: Javaslatok deaktiválva +* pl-PL: Sugestie wyłączone + +### account.visibility + +* de-DE: Sichtbarkeit +* en-GB: Visibility +* fr-FR: Visibilité +* hu-HU: Láthatóság +* pl-PL: Widoczność + +### account.webhook + +* de-DE: Webhook +* en-GB: Webhook +* fr-FR *missing* +* hu-HU: Webhook +* pl-PL: Webhook + +### account.webhook.disabled + +* de-DE: Nicht eingerichtet +* en-GB: No webhook configured +* fr-FR *missing* +* hu-HU: Nincs beállitva +* pl-PL: Nie skonfigurowano + +### account.webhook.active-pending + +* de-DE: Aktiv, noch nicht ausgeführt +* en-GB: Active; pending +* fr-FR *missing* +* hu-HU: Aktív, jóváhagyás folyamatban +* pl-PL: Aktywny, jeszcze nie wykonany + +### account.webhook.active-error + +* de-DE: Aktiv, fehlerhaft +* en-GB: Active; erroneous +* fr-FR *missing* +* hu-HU: Aktív, hibás +* pl-PL: Aktywny, błąd wykonania + +### account.webhook.active + +* de-DE: Aktiv +* en-GB: Active +* fr-FR *missing* +* hu-HU: Aktív +* pl-PL: Aktywny + +### account.traewelling.unsupported + +* de-DE: Wird wegen Inkompatibilität zwischen bahn.de und transitous derzeit nicht unterstützt +* en-GB: Unsupported due to incompatibility with bahn.de and transitous +* fr-FR *missing* +* hu-HU: Jelenleg nem támogatott a bahn.de és a transitous közötti inkompatibilitás miatt +* pl-PL: Obecnie nieobsługiwane z powodu niekompatybilności między bahn.de a transitous + +### account.registration-date + +* de-DE: Registriert am +* en-GB: Registered on +* fr-FR *missing* +* hu-HU: Regisztrálva +* pl-PL: Zarejestrowano dnia + +### account.interaction + +* de-DE: Interaktion +* en-GB: Interaction +* fr-FR: Intéraction +* hu-HU: Interakció +* pl-PL: Interakcje + +### account.interaction.accept-follows + +* de-DE: Accounts können dir direkt folgen +* en-GB: Accounts may follow you +* fr-FR: Autoriser à suivre +* hu-HU: Bárki követhet +* pl-PL: Inne konta mogą cię obserwować + +### account.interaction.accept-follow-requests + +* de-DE: Accounts können dir auf Anfrage folgen +* en-GB: Accounts may send follow requests +* fr-FR: Autoriser à recevoir une demande de suivi +* hu-HU: Bárki küldhet követési kéréseket +* pl-PL: Inne konta mogą wysyłać ci prośby o obserwowanie + +### account.interaction.one + +* de-DE: eine +* en-GB: one +* fr-FR: une +* hu-HU: egy +* pl-PL: jedna + +### account.interaction.open-request + +* de-DE: offene Anfrage +* en-GB: open request +* fr-FR: requête en attente +* hu-HU: követési kérés +* pl-PL: otwarta prośba + +### account.interaction.open-requests + +* de-DE: offene Anfragen +* en-GB: open requests +* fr-FR: requêtes en attente +* hu-HU: követési kérések +* pl-PL: otwarte prośby + +### account.interaction.disabled + +* de-DE: Accounts können dir nicht folgen +* en-GB: Accounts cannot follow you +* fr-FR: Aucun compte ne peut vous suivre +* hu-HU: Senki sem követhet +* pl-PL: Inne konta nie mogą cię obserwować + +### account.profile + +* de-DE: Öffentliches Profil +* en-GB: Public profile page +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Profil publiczny + +### account.interaction.requests + +* de-DE: Offene anfragen +* en-GB: Pending requests +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Otwarte zapytania + +### account.interaction.requests.incoming.none + +* de-DE: keine eingehend +* en-GB: none incoming +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: brak przychodzących + +### account.interaction.requests.incoming.some + +* de-DE: eingehend +* en-GB: incoming +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: przychodzące + +### account.interaction.requests.outgoing.none + +* de-DE: keine ausgehend +* en-GB: none outgoing +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: brak wychodzących + +### account.interaction.requests.outgoing.some + +* de-DE: ausgehend +* en-GB: outgoing +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: wychodzące + +## changelog.html.ep + +### changelog.added + +* de-DE: Neues Feature +* en-GB: New Feature +* fr-FR *missing* +* hu-HU: Új fícsör +* pl-PL: Nowa funkcja + +### changelog.bugfix + +* de-DE: Bugfix +* en-GB: Bugfix +* fr-FR *missing* +* hu-HU: Bugfix +* pl-PL: Poprawka błędu + +### changelog.2-16.1 + +* de-DE: Infrastruktur zur Übersetzung von travelynx in andere Sprachen. Der Großteil der Website ist noch Deutsch; einzelne Seiten sind bereits auf Englisch und Teilmengen davon zusätzlich auf Französisch, Polnisch und Ungarisch verfügbar. Weitere Übersetzungen und ggf. Sprachen sowie Dokumentation zur Unterstützung dabei folgen bei Zeiten. Die Sprache wird auf Basis des Accept-Language-Headers ausgewählt und kann zusätzlich in den Account-Einstellungen konfiguriert werden. +* en-GB: Localization support. Parts of travelynx are now available in English, and a subset of those is also available in French, Hungarian, and Polish. Further translations, languages, and translation how-tos will follow in due time. Locale selection respects the Accept-Language header and can be configured on the account settings page. +* fr-FR *missing* +* hu-HU: Lokalizáció. A travelynx most már angolul, részben pedig franciául, magyarul és lengyelül is elérhető. További fordítások, nyelvek és fordítási útmutatók következnek majd a megfelelő időben. A lokalizáció kiválasztása tiszteletben tartja az Accept-Language fejlécet, és a fiókbeállítások oldalon is beállítható. +* pl-PL: Infrastruktura do tłumaczenia Travelynx na inne języki. Większość strony nadal jest po niemiecku; pojedyncze strony są już dostępne po angielsku, a ich częściowo także po francusku, polsku i węgiersku. Kolejne tłumaczenia oraz ewentualne nowe języki i dokumentacja wspierająca ten proces pojawią się z czasem. Język jest wybierany na podstawie nagłówka Accept-Language i może być dodatkowo skonfigurowany w ustawieniach konta. + +### changelog.2-16.2 + +* de-DE: Korrekte Berechnung und Visualisierung der Wegstrecke bei Fahrten mit Ringlinien. Die Anzeige der Fahrten in der Fahrtenkarte ist noch teilweise fehlerhaft. +* en-GB: Fix distance calculation and visualization of ring line trips. Trips shown in the history map are still partially incorrect. +* fr-FR *missing* +* hu-HU: A távolságszámítás és a gyűrűsvonali utazások megjelenítése javítva lett. Az előzménytérképen megjelenített utazások még mindig részben hibásak. +* pl-PL: Poprawne obliczanie i wizualizacja trasy przejazdu dla linii okrężnych. Wyświetlanie przejazdów na mapie podróży nadal zawiera częściowe błędy. + +### changelog.2-17.1 + +* de-DE: Kartendaten („Polylines“) zu einzelnen Fahrten können nun als GPX ex- und importiert werden. Somit können Kartendaten nachträglich korrigiert oder nachgepflegt werden. Das GPX-Datenformat ist mit BRouter-Web kompatibel. +* en-GB: Map data (“polylines”) for individual trips can now be exported and imported in the GPX format. This allows map data to be corrected or augmented after the fact. The GPX import/export is compatible with BRouter-Web. +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Dane mapy („Polylines”) dla pojedynczych przejazdów można teraz eksportować i importować jako pliki GPX. Dzięki temu dane mapy można później poprawić lub uzupełnić. Format danych GPX jest kompatybilny z BRouter-Web. + +## journey.html.ep + +### journey.not-found + +* de-DE: Fahrt nicht gefunden. +* en-GB: Trip not found. +* fr-FR *missing* +* hu-HU: Utazás nem találva. +* pl-PL: Przejazd nie został znaleziony. + +### journey.trip + +*short* + +* de-DE: Fahrt +* en-GB: Trip +* fr-FR *missing* +* hu-HU: Utazás +* pl-PL: Przejazd + +### journey.from + +*short* + +* de-DE: Von +* en-GB: From +* fr-FR *missing* +* hu-HU: Honnan +* pl-PL: Z + +### journey.to + +*short* + +* de-DE: Nach +* en-GB: To +* fr-FR *missing* +* hu-HU: Hova +* pl-PL: Do + +### journey.departure + +*short* + +* de-DE: Abfahrt +* en-GB: Departure +* fr-FR *missing* +* hu-HU: Indulás +* pl-PL: Odjazd + +### journey.arrival + +*short* + +* de-DE: Ankunft +* en-GB: Arrival +* fr-FR *missing* +* hu-HU: Érkezés +* pl-PL: Przyjazd + +### journey.distance + +*short* + +* de-DE: Strecke +* en-GB: Distance +* fr-FR *missing* +* hu-HU: Távolság +* pl-PL: Dystans + +### journey.beeline.pre + +* de-DE: (Luftlinie: +* en-GB: ( +* fr-FR *missing* +* hu-HU: (Légvonalban: +* pl-PL: (Linia prosta: + +### journey.beeline.post + +* de-DE: ) +* en-GB: as the crow flies) +* fr-FR *missing* +* hu-HU: ) +* pl-PL: ) + +### journey.speed + +*short* + +* de-DE: Tempo +* en-GB: Speed +* fr-FR *missing* +* hu-HU: Sebesség +* pl-PL: Prędkość + +### journey.operator + +*short* + +* de-DE: Betrieb +* en-GB: Operator +* fr-FR *missing* +* hu-HU: Üzemeltető +* pl-PL: Operator + +### journey.messages + +*short* + +* de-DE: Meldungen +* en-GB: Messages +* fr-FR *missing* +* hu-HU: Jelentések +* pl-PL: Komunikaty + +### journey.comment + +*short* + +* de-DE: Kommentar +* en-GB: Comment +* fr-FR *missing* +* hu-HU: Megjegyzés +* pl-PL: Komentarz + +### journey.carriages + +*short* + +* de-DE: Rollmaterial +* en-GB: Carriages +* fr-FR *missing* +* hu-HU: Járművek +* pl-PL: Skład pociągu + +### journey.route + +*short* + +* de-DE: Route +* en-GB: Route +* fr-FR *missing* +* hu-HU: Útvonal +* pl-PL: Trasa + +### journey.share + +* de-DE: Teilen +* en-GB: Share +* fr-FR *missing* +* hu-HU: Megosztás +* pl-PL: Udostępnij + +### journey.export + +* de-DE: Exportieren +* en-GB: Export +* fr-FR *missing* +* hu-HU: Exportálás +* pl-PL: Eksportuj + +### journey.edit + +* de-DE: Bearbeiten +* en-GB: Edit +* fr-FR *missing* +* hu-HU: Szerkesztés +* pl-PL: Edytuj + +### journey.map-data + +* de-DE: Kartendaten +* en-GB: Map Data +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Dane mapy + +### journey.map.download + +* de-DE: Herunterladen +* en-GB: Download +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Pobierz + +### journey.map.upload + +* de-DE: Hochladen +* en-GB: Upload +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Prześlij + +### journey.map.upload-full + +* de-DE: Komplette Route +* en-GB: Upload full route +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Cała trasa + +### journey.map.upload-partial + +* de-DE: Gefahrenes Segment +* en-GB: Upload travelled segment +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Przejechany odcinek + +### journey.map.info.download + +* de-DE: JSON-Format: [[lon, lat, ID], ...] in WGS84-Koordinaten. GPX-Dateien sind mit BRouter kompatibel. +* en-GB: JSON format: [[lon, lat, station ID], ...], with lon/lat in WGS84 coordinates. GPX files are compatible with BRouter. +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Format JSON: [[lon, lat, ID], ...] w współrzędnych WGS84. Pliki GPX są kompatybilne z BRouter. + +### journey.map.info.upload + +* de-DE: GPX-Uploads müssen ein einzelnes track-Element mit einem einzelnen track segment enthalten. Ein BRouter-GPX-Export erfüllt diese Vorgaben. Uploads müssen entweder die komplette Route des Verkehrsmittels oder nur den zu diesem Checkin zugehörigen Abschnitt enthalten. Beim Hochladen bitte die passende Schaltfläche auswählen. IDs von Halten müssen beim Upload nicht angegeben werden. Bitte beachten: Beim Einspielen eigener Kartendaten werden die zuvor gespeicherten unwiderruflich gelöscht. +* en-GB: GPX uploads must contain a single track with a single track segment (such as provided by BRouter's export). They must cover either the full route or just the travelled route segment that belongs to this checkin. Please use the appropriate upload button, otherwise chaos may ensue. There is no need to specify station IDs when uploading tracks. Note that uploads irreversibly replace previously stored map data. +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Pliki GPX muszą zawierać jeden element track z jednym segmentem track. Eksport GPX z BRouter spełnia te wymagania. Przesyłane dane muszą zawierać całą trasę środka transportu lub tylko odcinek odpowiadający temu checkinowi. Przy przesyłaniu wybierz odpowiedni przycisk. ID przystanków nie muszą być podane. Uwaga: przesłanie własnych danych mapy bezpowrotnie usuwa wcześniej zapisane dane. + +### journey.danger + +* de-DE: Danger Zone +* en-GB: Danger Zone +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Danger Zone + +### journey.delete + +* de-DE: Löschen +* en-GB: Delete +* fr-FR *missing* +* hu-HU: Törlés +* pl-PL: Usuń + +## landingpage.html.ep + +### landingpage.greeting-prefix + +* de-DE: Hallo, +* en-GB: Hello, +* fr-FR *missing* +* hu-HU: Üdv, +* pl-PL: Cześć, + +### landingpage.greeting-suffix + +* de-DE: ! +* en-GB: ! +* fr-FR *missing* +* hu-HU: ! +* pl-PL: ! + +### landingpage.not-checked-in + +* de-DE: Du bist gerade nicht eingecheckt +* en-GB: You are not checked in at the moment +* fr-FR *missing* +* hu-HU: Jelenleg nem vagy becsekkolva +* pl-PL: Nie jesteś obecnie zameldowany. + +### landingpage.stop-geosearch + +* de-DE: Stationen in der Umgebung suchen +* en-GB: Look for stops nearby +* fr-FR *missing* +* hu-HU: Közeledben lévő megállók keresése +* pl-PL: Szukaj stacji w pobliżu + +### landingpage.manual-stop-entry + +* de-DE: Manuelle Eingabe +* en-GB: Enter stop manually +* fr-FR *missing* +* hu-HU: Megálló manuális bevitele +* pl-PL: Wprowadź ręcznie + +### landingpage.departures + +*As short as possible* + +* de-DE: Abfahrten +* en-GB: Departures +* fr-FR *missing* +* hu-HU: Járatok innen +* pl-PL: Odjazdy + +### landingpage.latest-trips + +* de-DE: Letzte Fahrten +* en-GB: Latest Trips +* fr-FR *missing* +* hu-HU: Legutóbbi utazások +* pl-PL: Ostatnie przejazdy + +### landingpage.date-format + +* de-DE: %d.%m.%Y +* en-GB: %b %d %Y +* fr-FR *missing* +* hu-HU: %Y.%m.%d +* pl-PL: %d.%m.%Y + +### landingpage.about + +* de-DE: 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. +* en-GB: Travelynx facilitates checkins into public transit (such as buses, trams, or trains) in Germany, Austria, Switzerlanz, Luxembourg, Ireland, Denmark, parts of the USA, and more. This way, you can track your own journeys (often including map and real-time data), share them with others, and examine highly relevant questions such as “how many hours did I spend in public transit in the past month?”. +* fr-FR *missing* +* hu-HU: Travelynx-el tömegközlekedési eszközökre (pl. Buszokra, Villamosokra, Vonatokra) be lehet csekkolni, többek között Németországban, Ausztriában, Svájcban, Luxemburgban, Írországban, Dániában és az USA egyes részeiben. Így nyomon követheted az utazásaidat (gyakran térképes és valós idejű adatokkal együtt), megoszthatod azokat másokkal, és a rendkívüli fontos kérdéseidre, mint például „hány órát tömegközlekedtem az elmúlt hónapban?”, is kapsz választ. +* pl-PL: Travelynx umożliwia zameldowanie się w środkach transportu (autobusach, tramwajach, pociągach) między innymi w Niemczech, Austrii, Szwajcarii, Luksemburgu, Irlandii, Danii i niektórych częściach Stanów Zjednoczonych. Dzięki temu można później śledzić własne podróże wraz z danymi w czasie rzeczywistym i komunikatami serwisowymi oraz uzyskać odpowiedzi na palące pytania, takie jak „Ile godzin spędziłem w podróży w zeszłym miesiącu?”. + +### landingpage.traewelling.pre + +* de-DE: Die Idee dazu kommt von +* en-GB: The idea for such a service was first proposed and implemented by +* fr-FR *missing* +* hu-HU: Az elsők akiknek hasonló ötlete volt és azt kivitelezték a +* pl-PL: Pomysł pochodzi od + +### landingpage.traewelling.post + +* de-DE: +* en-GB: +* fr-FR *missing* +* hu-HU: volt +* pl-PL: + +### landingpage.features + +* de-DE: Features: +* en-GB: Features: +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Funkcje: + +### landingpage.features.log + +* de-DE: Protokoll von Fahrplan- und Echtzeitdaten an Start- und Zielbahnhof +* en-GB: Log of scheduled and real-time departure and arrival times at departure and destination stop +* fr-FR *missing* +* hu-HU: Menetrend szerinti és valós idejű adatok naplózása. (indulási és célállomásokon) +* pl-PL: Protokół danych rozkładu jazdy i danych w czasie rzeczywistym na stacji początkowej i końcowej + +### landingpage.features.share + +* de-DE: Teilen von aktuellen und vergangenen Fahrten mit anderen Personen +* en-GB: Sharing the current check-in and past journeys with others +* fr-FR *missing* +* hu-HU: Jelenlegi és korábbi utazások megosztása másokkal +* pl-PL: Udostępnianie aktualnych i przeszłych przejazdów innym osobom + +### landingpage.features.api-pre + +* de-DE: Web-Hooks und +* en-GB: Web-Hooks and an +* fr-FR *missing* +* hu-HU: Web-Hook-ok és egy +* pl-PL: Webhooki oraz + +### landingpage.features.api-link + +* de-DE: API +* en-GB: API +* fr-FR *missing* +* hu-HU: API +* pl-PL: API + +### landingpage.features.api-post + +* de-DE: zum automatisierten Einchecken und Auslesen des aktuellen Status +* en-GB: for automatic check-ins and passing the current status to other applications +* fr-FR *missing* +* hu-HU: az automatikus becsekkeléshez és az aktuális státusz leolvasásához +* pl-PL: do automatycznego zameldowania się i odczytu aktualnego statusu + +### landingpage.features.stats + +* de-DE: Statistiken über Reisezeiten und Verspätungen +* en-GB: Stats about journey times and delays +* fr-FR *missing* +* hu-HU: Statisztikák az utazások időtartamáról és a késésekről +* pl-PL: Statystyki dotyczące czasu podróży i opóźnień + +### landingpage.features.passenger-rights + +* de-DE: Unterstützung beim Ausfüllen von Fahrgastrechteformularen +* en-GB: Support when dealing with passenger rights forms +* fr-FR *missing* +* hu-HU: Támogatás az utasjogi formanyomtatványok kitöltéséhez +* pl-PL: Wsparcie przy wypełnianiu formularzy praw pasażera + +### landingpage.features.public + +* de-DE: Optional: Öffentlicher Reisestatus und öffentliche Angaben zu vergangenen Fahrten +* en-GB: Optional: public travel status and public data about past journeys +* fr-FR *missing* +* hu-HU: Opcionális: utazási státusz és a korábbi utazásokról szóló adatok nyilvánosítása +* pl-PL: Opcjonalnie: publiczny status podróży i publiczne dane o przeszłych przejazdach + +### landingpage.disclaimer.lead + +* de-DE: 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. +* en-GB: Travelynx is a hobby project. It is provided free of charge, without any kind of availability guarantees. Unexpected downtimes or a cancellation of the entire site on short notice are not planned, but always possible. +* fr-FR *missing* +* hu-HU: Travelynx egy ingyenes és bármi rendelkezésre állási garacia nélküli hobbi projekt. Be nem tervezett leállások, illetve a teljes oldal hírtelen bezárása nincs tervben, de bármikor előfordulhat. +* pl-PL: Travelynx jest bezpłatnym, prywatnym projektem bez gwarancji dostępności. Nieprzewidziane przerwy w działaniu lub krótkoterminowe wyłączenie tej strony nie są planowane, ale są możliwe. + +### landingpage.disclaimer.source-pre + +* de-DE: Wer mag, kann auch den +* en-GB: If you like, you can download the +* fr-FR *missing* +* hu-HU: Aki szeretné az letöltheti a +* pl-PL: Kto chce, może również pobrać + +### landingpage.disclaimer.source-link + +* de-DE: Quelltext +* en-GB: source code +* fr-FR *missing* +* hu-HU: forráskódot +* pl-PL: kod źródłowy + +### landingpage.disclaimer.source-post + +* de-DE: laden und eine eigene Instanz aufsetzen. +* en-GB: and host your own instance. +* fr-FR *missing* +* hu-HU: is és saját szervert működtethet. +* pl-PL: i skonfigurować własną instancję. + +## language.html.ep + +### language.language + +* de-DE: Sprache +* en-GB: Language +* fr-FR *missing* +* hu-HU: Nyelv +* pl-PL: Język + +### language.browser-default + +* de-DE: Gleiche Sprache wie Web-Browser +* en-GB: Use language(s) requested by browser +* fr-FR *missing* +* hu-HU: A webböngésző nyelve(i) +* pl-PL: Ten sam język co przeglądarka internetowa + +## login.html.ep + +### login.accept-tos-pre + +* de-DE: Mit der Anmeldung stimmst du den +* en-GB: By logging in, you accept the +* fr-FR: En vous connectant, vous acceptez les +* hu-HU: A bejelentkezéssel elfogadod a +* pl-PL: Logując się akceptujesz + +### login.tos + +* de-DE: Nutzungsbedingungen +* en-GB: terms of use +* fr-FR: conditions d'utilisation +* hu-HU: használati feltételeket +* pl-PL: zasady użytkowania + +### login.accept-tos-post + +* de-DE: zu. +* en-GB: +* fr-FR: +* hu-HU: +* pl-PL: + +### login.forgot-password + +* de-DE: Passwort vergessen +* en-GB: Forgot password +* fr-FR: Mot de passe oublié +* hu-HU: Elfelejtett jelszó +* pl-PL: Nie pamiętam hasła + +### login.registration-disabled + +* de-DE: Diese Instanz erlaubt derzeit keine Registrierung neuer Accounts +* en-GB: This instance does not allow registration of new accounts at the moment +* fr-FR: Cette instance n'accepte pas actuellement de nouvelles inscriptions. +* hu-HU: Ez a weboldal jelenleg nem fogad új felhasználókat +* pl-PL: Ta instancja nie zezwala w tej chwili na nowe rejestracje + +## register.html.ep + +### register.name + +* de-DE: Name (alphanumerisch) +* en-GB: Name (alphanumeric) +* fr-FR: Nom (alphanumeric) +* hu-HU: Név (alfanumerikus) +* pl-PL: Nazwa (alfanumeryczna) + +### register.mail + +* de-DE: E-Mail-Adresse +* en-GB: Email address +* fr-FR: Adresse e-mail +* hu-HU: E-Mail cím +* pl-PL: Adres e-mail + +### register.password + +* de-DE: Passwort +* en-GB: Password +* fr-FR: Mot de passe +* hu-HU: Jelszó +* pl-PL: Hasło + +### register.repeat-password + +* de-DE: Passwort wiederholen +* en-GB: Repeat password +* fr-FR: Vérifier le mot de passe +* hu-HU: Jelszó újra +* pl-PL: Powtórz hasło + +### register.accept-tos-pre + +* de-DE: Mit deiner Registrierung stimmst du den +* en-GB: By submitting this registration form, you accept the +* fr-FR: En envoyant ce formulaire d'inscription, vous acceptez les +* hu-HU: A regisztrálással elfogadod a +* pl-PL: Rejestrując się akceptujesz + +### register.tos + +* de-DE: Nutzungsbedingungen +* en-GB: terms of use +* fr-FR: conditions d'utilisation +* hu-HU: használati feltételeket +* pl-PL: zasady użytkowania + +### register.accept-tos-post + +* de-DE: zu. +* en-GB: . +* fr-FR: +* hu-HU: +* pl-PL: + +### register.expect-confirmation-link + +* de-DE: Nach der Registrierung wird ein für 48 Stunden gültiger Bestätigungslink an die angegebene Mail-Adresse geschickt. Eine Anmeldung ist erst nach Bestätigung der Mail-Adresse möglich. +* en-GB: After submitting the registration, a confirmation link will be sent to the provided email address. Logging into the new travelynx account is only possible after following that link. The link is valid for 48 hours. +* fr-FR: Après avoir envoyé votre demande d'inscription, un lien de confirmation sera envoyé à l'adresse e-mail fournie. Vous pourrez vous connecter sur votre compte travelynx après avoir ouvert ce lien. Le lien est valide pendant 48 heures. +* hu-HU: A regisztráció elküldése után egy megerősítő linket küldünk a megadott e-mail címre. Az új travelynx-fiókba való bejelentkezés csak a link követése után lehetséges. A link 48 órán át érvényes. +* pl-PL: Po przesłaniu rejestracji na podany adres e-mail zostanie wysłany link potwierdzający. Zalogowanie się do konta travelynx jest możliwe dopiero po kliknięciu tego linku. Link jest ważny przez 48 godzin. + +### register.why-mail + +* de-DE: Die Mail-Adresse wird ausschließlich zur Bestätigung der Anmeldung, für die „Passwort vergessen“-Funktionalität und für wichtige Informationen über den Account verwendet und nicht an Dritte weitergegeben. +* en-GB: We collect your email address solely fo the purposes of confirming your registration, resetting your password and providing you with important information about your account. Your email address will never be shared with third parties. +* fr-FR *missing* +* hu-HU: Az e-mail címet csak a regisztráció megerősítésére, az „Elfelejtett jelszó” funkcióhoz és a fiókkal kapcsolatos fontos információkhoz használjuk, és nem adjuk tovább harmadik félnek. +* pl-PL: Adres e-mail będzie wykorzystywany wyłącznie do potwierdzenia rejestracji, funkcji „Zapomniałem hasła” oraz ważnych informacji dotyczących konta i nie będzie udostępniany osobom trzecim. + +### register.privacy-pre + +* de-DE: Die +* en-GB: Our +* fr-FR *missing* +* hu-HU: Az +* pl-PL: + +### register.privacy + +* de-DE: Datenschutzerklärung +* en-GB: privacy policy +* fr-FR *missing* +* hu-HU: adatkezelési tájékoztató +* pl-PL: Polityka prywatności + +### register.privacy-post + +* de-DE: beschreibt weitere erhobene Daten sowie deren Zweck und Speicherfristen. +* en-GB: describes additional data that we collect, how we store it and for what purpose. +* fr-FR *missing* +* hu-HU: leírja az egyéb begyűjtött adatokat, valamit azok célját és tárolási időtartamát. +* pl-PL: opisuje inne gromadzone dane, a także ich cel i okresy przechowywania. + +### register.account-deletion + +* de-DE: Accounts werden nach einem Jahr ohne Aktivität per E-Mail über die bevorstehende Löschung informiert und nach vier weiteren Wochen ohne Aktivität automatisch gelöscht. +* en-GB: Accounts are automatically deleted after a year without activity. You will receive an email informing you about the deletion and giving you four weeks to intervene. +* fr-FR: . +* hu-HU: A fiókokat egy év inaktivitás után e-mailben értesítjük a közelgő törlésről, és további négy hét inaktivitás után automatikusan törlődnek. +* pl-PL: Po roku braku aktywności użytkownicy otrzymują wiadomość e-mail z informacją o zbliżającym się usunięciu konta, a po kolejnych czterech tygodniach braku aktywności konta są automatycznie usuwane. + +### register.disclaimer + +* de-DE: Bitte beachten: Travelynx ist ein privat betriebenes Projekt ohne Verfügbarkeitsgarantie. Unangekündigte Downtimes oder eine kurzfristige Einstellung dieser Seite sind nicht vorgesehen, aber möglich. +* en-GB: Please be aware: Travelynx is a non-commerical personal project and is provided as is. We do not make guarantees in regards to availability of the service or a sudden end of the project. +* fr-FR *missing* +* hu-HU: Kérjük, vedd figyelembe: Travelynx egy ingyenes és bármi rendelkezésre állási garacia nélküli hobbi projekt. Be nem tervezett leállások, illetve a teljes oldal hírtelen bezárása nincs tervben, de bármikor előfordulhat. +* pl-PL: Uwaga: Travelynx jest prywatnym projektem bez gwarancji dostępności. Niezapowiedziane przerwy w działaniu lub krótkoterminowe wyłączenie tej strony nie są planowane, ale są możliwe. + +## _checked_in.html.ep, _public_status_card.html.ep + +### status.is-checked-in + +* de-DE: ist unterwegs +* en-GB: is in transit +* fr-FR: en chemin +* hu-HU: útban van +* pl-PL: jest w podróży + +### status.is-not-checked-in + +* de-DE: ist gerade nicht eingecheckt +* en-GB: ist not in transit right now +* fr-FR: n'est pas en chemin +* hu-HU: jelenleg nincs becsekkolva +* pl-PL: nie jest w tej chwili w podróży + +### status.select-destination + +* de-DE: Ziel wählen +* en-GB: Choose destination +* fr-FR *missing* +* hu-HU: Cél kiválasztása +* pl-PL: Wybierz cel + +### status.share + +* de-DE: Teilen +* en-GB: Share +* fr-FR: Partager +* hu-HU: Megosztás +* pl-PL: Udostępnij + +### status.check-out + +* de-DE: Auschecken +* en-GB: check out +* fr-FR *missing* +* hu-HU: Kicsekkelés +* pl-PL: Wymelduj się + +### status.boarding-in.pre + +*noun or verb* + +* de-DE: Einfahrt in +* en-GB: arrives in +* fr-FR *missing* +* hu-HU: +* pl-PL: Wjazd za + +### status.boarding-in.post + +*noun or verb* + +* de-DE: +* en-GB: +* fr-FR *missing* +* hu-HU: -en belül érkezik +* pl-PL: + +### status.boarding-soon + +*noun or verb* + +* de-DE: fährt ein +* en-GB: now arriving +* fr-FR *missing* +* hu-HU: A beszállás hamarosan következik +* pl-PL: wjeżdża + +### status.departure-in.pre + +*noun or verb* + +* de-DE: Abfahrt in +* en-GB: departs in +* fr-FR *missing* +* hu-HU: +* pl-PL: Odjazd za + +### status.departure-in.post + +*noun or verb* + +* de-DE: +* en-GB: +* fr-FR *missing* +* hu-HU: -en belül indul +* pl-PL: + +### status.departure-soon + +*noun or verb* + +* de-DE: fährt ab +* en-GB: now departing +* fr-FR *missing* +* hu-HU: Egy percen belül indul +* pl-PL: odjeżdża + +### status.arrival-in.pre + +*noun or verb* + +* de-DE: Ankunft in +* en-GB: arrives in +* fr-FR: Arrive dans +* hu-HU: +* pl-PL: Dojazd za + +### status.arrival-in.post + +*noun or verb* + +* de-DE: +* en-GB: +* fr-FR: +* hu-HU: -en belül érkezik +* pl-PL: + +### status.arrival-soon + +*noun or verb* + +* de-DE: Ankunft in weniger als einer Minute +* en-GB: now arriving +* fr-FR: Arrive dans moins d'une minute +* hu-HU: Egy percen belül érkezik +* pl-PL: Dojazd za mniej niż minutę + +### status.arrival-unknown + +* de-DE: Ankunft unbekannt +* en-GB: Arrival unknown +* fr-FR: Heure d'arrivée inconnue +* hu-HU: Érkezési időpont ismeretlen +* pl-PL: Czas dojazdu nieznany + +### status.arrived + +* de-DE: Ziel erreicht +* en-GB: arrived +* fr-FR: Arrivé +* hu-HU: Cél elérve +* pl-PL: Cel osiągnięty + +### status.depart-from.pre + +* de-DE: von +* en-GB: from +* fr-FR *missing* +* hu-HU: +* pl-PL: z + +### status.depart-from.post + +* de-DE: +* en-GB: +* fr-FR *missing* +* hu-HU: vágányról indul +* pl-PL: + +### status.arrive-on.pre + +* de-DE: auf +* en-GB: on +* fr-FR *missing* +* hu-HU: +* pl-PL: na + +### status.arrive-on.post + +* de-DE: +* en-GB: +* fr-FR *missing* +* hu-HU: vágányra érkezik +* pl-PL: + +### status.delayed-auto-checkout + +* de-DE: Der automatische Checkout erfolgt spätestens eine halbe Stunde nach der Ankunft. +* en-GB: You will be automatically checked out no less than half an hour after the expected arrival time. +* fr-FR *missing* +* hu-HU: Körülbelül tíz perccel az érkezés után automatikusan kicsekkolódsz. +* pl-PL: Automatyczne wymeldowanie nastąpi najpóźniej pół godziny po przyjeździe. + +### status.realtime-unavailable + +* de-DE: Keine Echtzeitdaten vorhanden +* en-GB: Real-time data unavailable +* fr-FR *missing* +* hu-HU: Valós idejű adatok nem állnak rendelkezésre +* pl-PL: Brak danych w czasie rzeczywistym + +### status.messages + +*service messages related to the trip* + +* de-DE: Meldungen +* en-GB: Notifications +* fr-FR *missing* +* hu-HU: Jelentések +* pl-PL: Komunikaty + +### status.map + +* de-DE: Karte +* en-GB: Map +* fr-FR *missing* +* hu-HU: Térkép +* pl-PL: Mapa + +### status.change-destination + +* de-DE: Ziel ändern? +* en-GB: Change destination +* fr-FR *missing* +* hu-HU: Célváltás? +* pl-PL: Zmień cel? + +### status.carriages + +* de-DE: Wagen +* en-GB: Carriages +* fr-FR: Voitures +* hu-HU: Kocsik +* pl-PL: Wagony + +### status.route + +* de-DE: Route +* en-GB: Route +* fr-FR: Route +* hu-HU: Útvonal +* pl-PL: Trasa + +### status.undo + +*shown during destination selection* + +* de-DE: Rückgängig +* en-GB: Undo +* fr-FR *missing* +* hu-HU: Visszavonás +* pl-PL: Cofnij + +### status.privacy-level + +*shown during destination selection* + +* de-DE: Sichtbarkeit +* en-GB: Privacy +* fr-FR *missing* +* hu-HU: Láthatóság +* pl-PL: Widoczność + +### status.undo-checkin + +*shown once check-in is completed* + +* de-DE: Checkin rückgängig +* en-GB: Undo check-in +* fr-FR *missing* +* hu-HU: Becsekkelés visszavonása +* pl-PL: Cofnij meldowanie + +### status.force-checkout-lead + +* de-DE: Maximal eine halbe Stunde nach der Ankunft erfolgt ein automatischer Checkout. Falls das Backend ausgefallen ist oder die Fahrt aus anderen Gründen verloren ging: +* en-GB: travelynx will perform an automatic check-out no less than half an hour after arrival. In case of backend or data update issues: +* fr-FR *missing* +* hu-HU: Körülbelül tíz perccel az érkezés után automatikusan kicsekkolódsz. Ha a backend leállt, vagy az utazás más okokból elveszlett: +* pl-PL: Automatyczne wymeldowanie nastąpi najpóźniej pół godziny po przyjeździe. Jeśli backend nie działa lub przejazd został utracony z innych powodów: + +### status.force-checkout + +* de-DE: Jetzt auschecken +* en-GB: Force checkout +* fr-FR *missing* +* hu-HU: Most kicsekkelni +* pl-PL: Wymelduj się teraz + +## _history_stats.html.ep + +### stats.trips + +* de-DE: Fahrten +* en-GB: Trips +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Ilość przejazdów + +### stats.distance + +* de-DE: Entfernung +* en-GB: Distance +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Dystans + +### stats.time + +* de-DE: Fahrtzeit +* en-GB: Travel Time +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Czas przejazdów + +### stats.hours + +* de-DE: Stunden +* en-GB: hours +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: godzin + +### stats.per-schedule + +* de-DE: nach Fahrplan +* en-GB: scheduled +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: zgodnie z rozkładem + +### stats.transfer-time + +* de-DE: Wartezeit (Umstiege) +* en-GB: Transfer Time +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Czas oczekiwania (przesiadki) + +### stats.total-delay + +* de-DE: Kumulierte Verspätung +* en-GB: Total Delay +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Skumulowane opóźnienie + +### stats.on-dep + +* de-DE: Bei Abfahrt +* en-GB: on departure +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Przy odjeździe + +### stats.on-arr + +* de-DE: Bei Ankunft +* en-GB: on arrival +* fr-FR *missing* +* hu-HU *missing* +* pl-PL: Przy przyjeździe + +## _wagons.html.ep + +### wagons.name-as-type + +* de-DE: als +* en-GB: running as +* fr-FR: sous le nom de +* hu-HU: közlekedik mint: +* pl-PL: jedzie jako + +### wagons.from.pre + +* de-DE: von +* en-GB: from +* fr-FR: en provenance de +* hu-HU: +* pl-PL: z + +### wagons.from.post + +* de-DE: +* en-GB: +* fr-FR: +* hu-HU: -tól +* pl-PL: + +### wagons.to.pre + +* de-DE: nach +* en-GB: towards +* fr-FR: à destination de +* hu-HU: +* pl-PL: do + +### wagons.to.post + +* de-DE: +* en-GB: +* fr-FR: +* hu-HU: -ig +* pl-PL: + +### wagons.carriage + +* de-DE: Wagen +* en-GB: Carriage +* fr-FR: Voiture +* hu-HU: Kocsi +* pl-PL: Wagon diff --git a/share/locales/template.pot b/share/locales/template.pot new file mode 100644 index 0000000..f609776 --- /dev/null +++ b/share/locales/template.pot @@ -0,0 +1,623 @@ +msgid "" +msgstr "" +"Language: FIXME\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" + +# +# Global Strings +# + +msgid "strftime.datetime" +msgstr "" + +msgid "button.register" +msgstr "" + +msgid "button.login" +msgstr "" + +msgid "button.logout" +msgstr "" + +msgid "footer.imprint" +msgstr "" + +msgid "footer.privacy" +msgstr "" + +msgid "footer.legend" +msgstr "" + +msgid "footer.colour-scheme" +msgstr "" + +msgid "footer.colour-scheme.light" +msgstr "" + +msgid "footer.colour-scheme.dark" +msgstr "" + +msgid "footer.colour-scheme.auto" +msgstr "" + +msgid "header.error" +msgstr "" + +# +# Templates +# + +# about.html.ep + +msgid "about.developed-by.lead" +msgstr "" + +msgid "about.developed-by.and" +msgstr "" + +msgid "about.developed-by.others" +msgstr "" + +msgid "about.developed-by.tail" +msgstr "" + +msgid "about.source-code" +msgstr "" + +msgid "about.licence-agplv3" +msgstr "" + +msgid "about.data-sources" +msgstr "" + +msgid "about.data-sources.last-and" +msgstr "" + +msgid "about.disclaimer" +msgstr "" + +msgid "about.contact" +msgstr "" + +msgid "about.bugs" +msgstr "" + +msgid "about.changelog" +msgstr "" + +# account.html.ep + +msgid "account.changed-name" +msgstr "" + +msgid "account.changed-mail" +msgstr "" + +msgid "account.changed-password" +msgstr "" + +msgid "account.changed-language" +msgstr "" + +msgid "account.changed-privacy" +msgstr "" + +msgid "account.changed-social" +msgstr "" + +msgid "account.changed-traewelling" +msgstr "" + +msgid "account.changed-history" +msgstr "" + +msgid "account.changed-webhook" +msgstr "" + +msgid "account.cleared-notifications" +msgstr "" + +msgid "account.account" +msgstr "" + +msgid "account.name" +msgstr "" + +msgid "account.mail" +msgstr "" + +msgid "account.password" +msgstr "" + +msgid "account.language" +msgstr "" + +msgid "account.connections" +msgstr "" + +msgid "account.connections.enabled" +msgstr "" + +msgid "account.connections.disabled" +msgstr "" + +msgid "account.visibility" +msgstr "" + +msgid "account.webhook" +msgstr "" + +msgid "account.webhook.disabled" +msgstr "" + +msgid "account.webhook.active-pending" +msgstr "" + +msgid "account.webhook.active-error" +msgstr "" + +msgid "account.webhook.active" +msgstr "" + +msgid "account.traewelling.unsupported" +msgstr "" + +msgid "account.registration-date" +msgstr "" + +msgid "account.interaction" +msgstr "" + +msgid "account.interaction.accept-follows" +msgstr "" + +msgid "account.interaction.accept-follow-requests" +msgstr "" + +msgid "account.interaction.one" +msgstr "" + +msgid "account.interaction.open-request" +msgstr "" + +msgid "account.interaction.open-requests" +msgstr "" + +msgid "account.interaction.disabled" +msgstr "" + +msgid "account.profile" +msgstr "" + +msgid "account.interaction.requests" +msgstr "" + +msgid "account.interaction.requests.incoming.none" +msgstr "" + +msgid "account.interaction.requests.incoming.some" +msgstr "" + +msgid "account.interaction.requests.outgoing.none" +msgstr "" + +msgid "account.interaction.requests.outgoing.some" +msgstr "" + +# changelog.html.ep + +msgid "changelog.added" +msgstr "" + +msgid "changelog.bugfix" +msgstr "" + +msgid "changelog.2-16.1" +msgstr "" + +msgid "changelog.2-16.2" +msgstr "" + +msgid "changelog.2-17.1" +msgstr "" + +# journey.html.ep + +msgid "journey.not-found" +msgstr "" + +#, short +msgid "journey.trip" +msgstr "" + +#, short +msgid "journey.from" +msgstr "" + +#, short +msgid "journey.to" +msgstr "" + +#, short +msgid "journey.departure" +msgstr "" + +#, short +msgid "journey.arrival" +msgstr "" + +#, short +msgid "journey.distance" +msgstr "" + +msgid "journey.beeline.pre" +msgstr "" + +msgid "journey.beeline.post" +msgstr "" + +#, short +msgid "journey.speed" +msgstr "" + +#, short +msgid "journey.operator" +msgstr "" + +#, short +msgid "journey.messages" +msgstr "" + +#, short +msgid "journey.comment" +msgstr "" + +#, short +msgid "journey.carriages" +msgstr "" + +#, short +msgid "journey.route" +msgstr "" + +msgid "journey.share" +msgstr "" + +msgid "journey.export" +msgstr "" + +msgid "journey.edit" +msgstr "" + +msgid "journey.map-data" +msgstr "" + +msgid "journey.map.download" +msgstr "" + +msgid "journey.map.upload" +msgstr "" + +msgid "journey.map.upload-full" +msgstr "" + +msgid "journey.map.upload-partial" +msgstr "" + +msgid "journey.map.info.download" +msgstr "" + +msgid "journey.map.info.upload" +msgstr "" + +msgid "journey.danger" +msgstr "" + +msgid "journey.delete" +msgstr "" + +# landingpage.html.ep + +msgid "landingpage.greeting-prefix" +msgstr "" + +msgid "landingpage.greeting-suffix" +msgstr "" + +msgid "landingpage.not-checked-in" +msgstr "" + +msgid "landingpage.stop-geosearch" +msgstr "" + +msgid "landingpage.manual-stop-entry" +msgstr "" + +#, As short as possible +msgid "landingpage.departures" +msgstr "" + +msgid "landingpage.latest-trips" +msgstr "" + +msgid "landingpage.date-format" +msgstr "" + +msgid "landingpage.about" +msgstr "" + +msgid "landingpage.traewelling.pre" +msgstr "" + +msgid "landingpage.traewelling.post" +msgstr "" + +msgid "landingpage.features" +msgstr "" + +msgid "landingpage.features.log" +msgstr "" + +msgid "landingpage.features.share" +msgstr "" + +msgid "landingpage.features.api-pre" +msgstr "" + +msgid "landingpage.features.api-link" +msgstr "" + +msgid "landingpage.features.api-post" +msgstr "" + +msgid "landingpage.features.stats" +msgstr "" + +msgid "landingpage.features.passenger-rights" +msgstr "" + +msgid "landingpage.features.public" +msgstr "" + +msgid "landingpage.disclaimer.lead" +msgstr "" + +msgid "landingpage.disclaimer.source-pre" +msgstr "" + +msgid "landingpage.disclaimer.source-link" +msgstr "" + +msgid "landingpage.disclaimer.source-post" +msgstr "" + +# language.html.ep + +msgid "language.language" +msgstr "" + +msgid "language.browser-default" +msgstr "" + +# login.html.ep + +msgid "login.accept-tos-pre" +msgstr "" + +msgid "login.tos" +msgstr "" + +msgid "login.accept-tos-post" +msgstr "" + +msgid "login.forgot-password" +msgstr "" + +msgid "login.registration-disabled" +msgstr "" + +# register.html.ep + +msgid "register.name" +msgstr "" + +msgid "register.mail" +msgstr "" + +msgid "register.password" +msgstr "" + +msgid "register.repeat-password" +msgstr "" + +msgid "register.accept-tos-pre" +msgstr "" + +msgid "register.tos" +msgstr "" + +msgid "register.accept-tos-post" +msgstr "" + +msgid "register.expect-confirmation-link" +msgstr "" + +msgid "register.why-mail" +msgstr "" + +msgid "register.privacy-pre" +msgstr "" + +msgid "register.privacy" +msgstr "" + +msgid "register.privacy-post" +msgstr "" + +msgid "register.account-deletion" +msgstr "" + +msgid "register.disclaimer" +msgstr "" + +# _checked_in.html.ep, _public_status_card.html.ep + +msgid "status.is-checked-in" +msgstr "" + +msgid "status.is-not-checked-in" +msgstr "" + +msgid "status.select-destination" +msgstr "" + +msgid "status.share" +msgstr "" + +msgid "status.check-out" +msgstr "" + +#, noun or verb +msgid "status.boarding-in.pre" +msgstr "" + +#, noun or verb +msgid "status.boarding-in.post" +msgstr "" + +#, noun or verb +msgid "status.boarding-soon" +msgstr "" + +#, noun or verb +msgid "status.departure-in.pre" +msgstr "" + +#, noun or verb +msgid "status.departure-in.post" +msgstr "" + +#, noun or verb +msgid "status.departure-soon" +msgstr "" + +#, noun or verb +msgid "status.arrival-in.pre" +msgstr "" + +#, noun or verb +msgid "status.arrival-in.post" +msgstr "" + +#, noun or verb +msgid "status.arrival-soon" +msgstr "" + +msgid "status.arrival-unknown" +msgstr "" + +msgid "status.arrived" +msgstr "" + +msgid "status.depart-from.pre" +msgstr "" + +msgid "status.depart-from.post" +msgstr "" + +msgid "status.arrive-on.pre" +msgstr "" + +msgid "status.arrive-on.post" +msgstr "" + +msgid "status.delayed-auto-checkout" +msgstr "" + +msgid "status.realtime-unavailable" +msgstr "" + +#, service messages related to the trip +msgid "status.messages" +msgstr "" + +msgid "status.map" +msgstr "" + +msgid "status.change-destination" +msgstr "" + +msgid "status.carriages" +msgstr "" + +msgid "status.route" +msgstr "" + +#, shown during destination selection +msgid "status.undo" +msgstr "" + +#, shown during destination selection +msgid "status.privacy-level" +msgstr "" + +#, shown once check-in is completed +msgid "status.undo-checkin" +msgstr "" + +msgid "status.force-checkout-lead" +msgstr "" + +msgid "status.force-checkout" +msgstr "" + +# _history_stats.html.ep + +msgid "stats.trips" +msgstr "" + +msgid "stats.distance" +msgstr "" + +msgid "stats.time" +msgstr "" + +msgid "stats.hours" +msgstr "" + +msgid "stats.per-schedule" +msgstr "" + +msgid "stats.transfer-time" +msgstr "" + +msgid "stats.total-delay" +msgstr "" + +msgid "stats.on-dep" +msgstr "" + +msgid "stats.on-arr" +msgstr "" + +# _wagons.html.ep + +msgid "wagons.name-as-type" +msgstr "" + +msgid "wagons.from.pre" +msgstr "" + +msgid "wagons.from.post" +msgstr "" + +msgid "wagons.to.pre" +msgstr "" + +msgid "wagons.to.post" +msgstr "" + +msgid "wagons.carriage" +msgstr "" diff --git a/t/authenticated-de.t b/t/authenticated-de.t new file mode 100644 index 0000000..f6a9c5c --- /dev/null +++ b/t/authenticated-de.t @@ -0,0 +1,112 @@ +#!/usr/bin/env perl + +# Copyright (C) 2025 Birte Kristina Friesel <derf@finalrewind.org> +# +# SPDX-License-Identifier: MIT + +use Mojo::Base -strict; + +use Test::More; +use Test::Mojo; + +# Include application +use FindBin; +require "$FindBin::Bin/../index.pl"; + +my $t = Test::Mojo->new('Travelynx'); + +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + +if ( not $t->app->config->{db} ) { + plan( skip_all => 'No database configured' ); +} + +# Account boilerplate + +$t->app->pg->db->query('drop schema if exists travelynx_test_auth_de cascade'); +$t->app->pg->db->query('create schema travelynx_test_auth_de'); +$t->app->pg->db->query('set search_path to travelynx_test_auth_de'); +$t->app->pg->on( + connection => sub { + my ( $pg, $dbh ) = @_; + $dbh->do('set search_path to travelynx_test_auth_de'); + } +); + +$t->app->config->{mail}->{disabled} = 1; + +$ENV{__TRAVELYNX_TEST_MINI_IRIS} = 1; +$t->app->start( 'database', 'migrate' ); + +my $csrf_token + = $t->ua->get('/register')->res->dom->at('input[name=csrf_token]') + ->attr('value'); + +# Successful registration +$t->post_ok( + '/register' => form => { + csrf_token => $csrf_token, + dt => 1, + user => 'someone', + email => 'foo@example.org', + password => 'foofoofoo', + password2 => 'foofoofoo', + } +); +$t->status_is(200)->content_like(qr{Verifizierungslink}); + +my $res = $t->app->pg->db->select( 'users', ['id'], { name => 'someone' } ); +my $uid = $res->hash->{id}; +$res = $t->app->pg->db->select( 'pending_registrations', ['token'], + { user_id => $uid } ); +my $token = $res->hash->{token}; + +# Successful verification +$t->get_ok("/reg/${uid}/${token}"); +$t->status_is(200)->content_like(qr{freigeschaltet}); + +# Successful login +$t->post_ok( + '/login' => form => { + csrf_token => $csrf_token, + user => 'someone', + password => 'foofoofoo', + } +); +$t->status_is(302)->header_is( location => '/' ); + +# Actual Test + +$t->get_ok('/account')->status_is(200); +$t->text_like( 'a[href="/p/someone"]' => qr{Öffentliches Profil} ); +$t->text_like( 'a[href="/api"]' => qr{Dokumentation} ); + +for my $subpage (qw(privacy social profile hooks insight language)) { + $t->get_ok("/account/${subpage}")->status_is(200); + $t->text_like( 'button' => qr{Speichern} ); +} + +for my $subpage (qw(password mail name)) { + $t->get_ok("/account/${subpage}")->status_is(200); + $t->text_like( 'button' => qr{Ändern} ); +} + +$t->get_ok('/account/select_backend')->status_is(200); +$t->text_like( 'a[href="#help"]' => qr{Details} ); + +$t->get_ok('/account/traewelling')->status_is(200); +$t->text_like( 'button' => qr{Verknüpfen} ); + +$t->get_ok('/history')->status_is(200); +$t->text_like( 'a[href="/history/map"]' => qr{Fahrtenkarte} ); + +$t->get_ok('/history/map')->status_is(200); +$t->text_like( 'button[type="submit"]' => qr{Anzeigen} ); + +$t->get_ok('/history/commute')->status_is(200); +$t->text_like( 'button[type="submit"]' => qr{Anzeigen} ); + +$t->get_ok('/journey/add')->status_is(200); +$t->text_like( 'button[type="submit"]' => qr{Hinzufügen} ); + +done_testing(); diff --git a/t/12-journey-edit.t b/t/journey-edit.t index f8b8ee5..f0b2f36 100644 --- a/t/12-journey-edit.t +++ b/t/journey-edit.t @@ -20,6 +20,8 @@ use utf8; my $t = Test::Mojo->new('Travelynx'); +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + if ( not $t->app->config->{db} ) { plan( skip_all => 'No database configured' ); } @@ -124,12 +126,19 @@ my ( $success, $error ) = $t->app->journeys->add( 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->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 => { @@ -138,10 +147,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'); @@ -161,12 +174,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 => { @@ -175,10 +195,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'); @@ -198,13 +222,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: \s*\n*\s* ..:36\)}mx) + ->content_like(qr{..:33 \s*\n*\s* \( -1, \s*Plan: \s*\n*\s* ..:34\)}mx) + ->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/11-journey-stats.t b/t/journey-stats.t index 4623402..205bf96 100644 --- a/t/11-journey-stats.t +++ b/t/journey-stats.t @@ -17,6 +17,8 @@ require "$FindBin::Bin/../index.pl"; my $t = Test::Mojo->new('Travelynx'); +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + if ( not $t->app->config->{db} ) { plan( skip_all => 'No database configured' ); } @@ -82,35 +84,48 @@ $t->post_ok( action => 'save', train => 'RE 42 11238', dep_station => 'EMSTP', - sched_departure => '16.10.2018 17:36', - rt_departure => '16.10.2018 17:36', + sched_departure => '2018-10-16T17:36', + rt_departure => '2018-10-16T17:36', arr_station => 'EG', - sched_arrival => '16.10.2018 18:34', - rt_arrival => '16.10.2018 18:34', + sched_arrival => '2018-10-16T18:34', + rt_arrival => '2018-10-16T18:34', comment => 'Passierschein A38', } ); $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}) +$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{Passierschein A38}) ->content_like(qr{Daten wurden manuell eingetragen}); -$t->get_ok('/history/2018/10')->status_is(200)->content_like(qr{62 km}) - ->content_like(qr{00:58 Stunden})->content_like(qr{00:00 Stunden}) +$t->get_ok('/history/2018/10') + ->status_is(200) + ->content_like(qr{62 km}) + ->content_like(qr{00:58 Stunden}) + ->content_like(qr{00:00 Stunden}) ->content_like(qr{Bei Abfahrt: 00:00 Stunden}) ->content_like(qr{Bei Ankunft: 00:00 Stunden}); -$t->get_ok('/history/2018')->status_is(200)->content_like(qr{62 km}) - ->content_like(qr{00:58 Stunden})->content_like(qr{00:00 Stunden}) +$t->get_ok('/history/2018') + ->status_is(200) + ->content_like(qr{62 km}) + ->content_like(qr{00:58 Stunden}) + ->content_like(qr{00:00 Stunden}) ->content_like(qr{Bei Abfahrt: 00:00 Stunden}) ->content_like(qr{Bei Ankunft: 00:00 Stunden}); -$t->get_ok('/history/map')->status_is(200) +$t->get_ok('/history/map') + ->status_is(200) ->content_like(qr{\[\[51.956[^,]*,7.635[^]]*\],'M.nster\(Westf\)Hbf'\],}) ->content_like(qr{\[\[51.504[^,]*,7.102[^]]*\],'Gelsenkirchen Hbf'\]}); @@ -123,23 +138,29 @@ $t->post_ok( action => 'save', train => 'RE 42 11238', dep_station => 'EMSTP', - sched_departure => '16.11.2018 17:36', - rt_departure => '16.11.2018 17:45', + sched_departure => '2018-11-16T17:36', + rt_departure => '2018-11-16T17:45', arr_station => 'EG', - sched_arrival => '16.11.2018 18:34', - rt_arrival => '16.11.2018 19:00', + sched_arrival => '2018-11-16T18:34', + rt_arrival => '2018-11-16T19:00', } ); $t->status_is(302)->header_is( location => '/journey/2' ); -$t->get_ok('/history/2018/11')->status_is(200)->content_like(qr{62 km}) - ->content_like(qr{01:15 Stunden})->content_like(qr{nach Fahrplan: 00:58}) +$t->get_ok('/history/2018/11') + ->status_is(200) + ->content_like(qr{62 km}) + ->content_like(qr{01:15 Stunden}) + ->content_like(qr{nach Fahrplan: 00:58}) ->content_like(qr{00:00 Stunden}) ->content_like(qr{Bei Abfahrt: 00:09 Stunden}) ->content_like(qr{Bei Ankunft: 00:26 Stunden}); -$t->get_ok('/history/2018')->status_is(200)->content_like(qr{124 km}) - ->content_like(qr{02:13 Stunden})->content_like(qr{nach Fahrplan: 01:56}) +$t->get_ok('/history/2018') + ->status_is(200) + ->content_like(qr{124 km}) + ->content_like(qr{02:13 Stunden}) + ->content_like(qr{nach Fahrplan: 01:56}) ->content_like(qr{00:00 Stunden}) ->content_like(qr{Bei Abfahrt: 00:09 Stunden}) ->content_like(qr{Bei Ankunft: 00:26 Stunden}); @@ -153,23 +174,29 @@ $t->post_ok( action => 'save', train => 'ICE 1', dep_station => 'EE', - sched_departure => '17.11.2018 15:42', + sched_departure => '2018-11-17T15:42', rt_departure => '', arr_station => 'BL', - sched_arrival => '17.11.2018 19:42', + sched_arrival => '2018-11-17T19:42', rt_arrival => '', } ); $t->status_is(302)->header_is( location => '/journey/3' ); -$t->get_ok('/history/2018/11')->status_is(200)->content_like(qr{513 km}) - ->content_like(qr{05:15 Stunden})->content_like(qr{nach Fahrplan: 04:58}) +$t->get_ok('/history/2018/11') + ->status_is(200) + ->content_like(qr{513 km}) + ->content_like(qr{05:15 Stunden}) + ->content_like(qr{nach Fahrplan: 04:58}) ->content_like(qr{00:00 Stunden}) ->content_like(qr{Bei Abfahrt: 00:09 Stunden}) ->content_like(qr{Bei Ankunft: 00:26 Stunden}); -$t->get_ok('/history/2018')->status_is(200)->content_like(qr{576 km}) - ->content_like(qr{06:13 Stunden})->content_like(qr{nach Fahrplan: 05:56}) +$t->get_ok('/history/2018') + ->status_is(200) + ->content_like(qr{576 km}) + ->content_like(qr{06:13 Stunden}) + ->content_like(qr{nach Fahrplan: 05:56}) ->content_like(qr{00:00 Stunden}) ->content_like(qr{Bei Abfahrt: 00:09 Stunden}) ->content_like(qr{Bei Ankunft: 00:26 Stunden}); diff --git a/t/23-journey-visibility.t b/t/journey-visibility.t index 1cc7e64..c2294b3 100644 --- a/t/23-journey-visibility.t +++ b/t/journey-visibility.t @@ -20,6 +20,8 @@ require "$FindBin::Bin/../index.pl"; my $t = Test::Mojo->new('Travelynx'); +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + if ( not $t->app->config->{db} ) { plan( skip_all => 'No database configured' ); } @@ -110,20 +112,24 @@ sub test_journey_visibility { $opt{effective_visibility_str}, $desc ); if ( $opt{public} ) { - $t->get_ok("/p/test1/j/$jid")->status_is(200) + $t->get_ok("/p/test1/j/$jid") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/p/test1/j/$jid")->status_is(404) + $t->get_ok("/p/test1/j/$jid") + ->status_is(404) ->content_like(qr{Fahrt nicht gefunden.}); } if ( $opt{with_token} ) { - $t->get_ok("/p/test1/j/$jid$token")->status_is(200) + $t->get_ok("/p/test1/j/$jid$token") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/p/test1/j/$jid$token")->status_is(404) + $t->get_ok("/p/test1/j/$jid$token") + ->status_is(404) ->content_like(qr{Fahrt nicht gefunden.}); } @@ -134,21 +140,25 @@ 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) + $t->get_ok("/p/test1/j/$jid") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/p/test1/j/$jid")->status_is(404) + $t->get_ok("/p/test1/j/$jid") + ->status_is(404) ->content_like(qr{Fahrt nicht gefunden.}); } # 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) + $t->get_ok("/p/test1/j/$jid$token") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/p/test1/j/$jid$token")->status_is(404) + $t->get_ok("/p/test1/j/$jid$token") + ->status_is(404) ->content_like(qr{Fahrt nicht gefunden.}); } @@ -160,21 +170,25 @@ 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) + $t->get_ok("/p/test1/j/$jid") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/p/test1/j/$jid")->status_is(404) + $t->get_ok("/p/test1/j/$jid") + ->status_is(404) ->content_like(qr{Fahrt nicht gefunden.}); } # 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) + $t->get_ok("/p/test1/j/$jid$token") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/p/test1/j/$jid$token")->status_is(404) + $t->get_ok("/p/test1/j/$jid$token") + ->status_is(404) ->content_like(qr{Fahrt nicht gefunden.}); } @@ -186,21 +200,25 @@ 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) + $t->get_ok("/p/test1/j/$jid") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/p/test1/j/$jid")->status_is(404) + $t->get_ok("/p/test1/j/$jid") + ->status_is(404) ->content_like(qr{Fahrt nicht gefunden.}); } # 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) + $t->get_ok("/p/test1/j/$jid$token") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/p/test1/j/$jid$token")->status_is(404) + $t->get_ok("/p/test1/j/$jid$token") + ->status_is(404) ->content_like(qr{Fahrt nicht gefunden.}); } diff --git a/t/24-past-visibility.t b/t/past-visibility.t index cf981b9..5c084b2 100644 --- a/t/24-past-visibility.t +++ b/t/past-visibility.t @@ -20,6 +20,8 @@ require "$FindBin::Bin/../index.pl"; my $t = Test::Mojo->new('Travelynx'); +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + if ( not $t->app->config->{db} ) { plan( skip_all => 'No database configured' ); } @@ -115,11 +117,13 @@ sub test_history_visibility { = "history vis=$opt{set_past_visibility} journey=$jid vis=$journey->{effective_visibility_str}"; if ( $opt{public} ) { - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_like( qr{DPN\s*667}, "public $desc" ); } else { - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_unlike( qr{DPN\s*667}, "public $desc" ); } @@ -129,11 +133,13 @@ sub test_history_visibility { ); if ( $opt{self} ) { - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_like( qr{DPN\s*667}, "self $desc" ); } else { - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_unlike( qr{DPN\s*667}, "self $desc" ); } @@ -144,11 +150,13 @@ sub test_history_visibility { ); if ( $opt{followers} ) { - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_like( qr{DPN\s*667}, "follower $desc" ); } else { - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_unlike( qr{DPN\s*667}, "follower $desc" ); } @@ -159,11 +167,13 @@ sub test_history_visibility { ); if ( $opt{travelynx} ) { - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_like( qr{DPN\s*667}, "travelynx $desc" ); } else { - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_unlike( qr{DPN\s*667}, "travelynx $desc" ); } diff --git a/t/r-negative-delay.t b/t/r-negative-delay.t index 4f9d94e..9f87510 100644 --- a/t/r-negative-delay.t +++ b/t/r-negative-delay.t @@ -17,6 +17,8 @@ require "$FindBin::Bin/../index.pl"; my $t = Test::Mojo->new('Travelynx'); +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + if ( not $t->app->config->{db} ) { plan( skip_all => 'No database configured' ); } @@ -83,17 +85,20 @@ $t->post_ok( action => 'save', train => 'RE 42 11238', dep_station => 'EMSTP', - sched_departure => '16.10.2018 17:36', - rt_departure => '16.10.2018 17:35', + sched_departure => '2018-10-16T17:36', + rt_departure => '2018-10-16T17:35', arr_station => 'EG', - sched_arrival => '16.10.2018 18:34', - rt_arrival => '16.10.2018 18:32', + sched_arrival => '2018-10-16T18:34', + rt_arrival => '2018-10-16T18:32', } ); -$t->status_is(302)->header_is( location => '/journey/1' ); +$t->status_is(302)->header_is( location => '/journey/1' )->content_is(q{}); -$t->get_ok('/history/2018/10')->status_is(200)->content_like(qr{62 km}) - ->content_like(qr{00:57 Stunden})->content_like(qr{nach Fahrplan: 00:58}) +$t->get_ok('/history/2018/10') + ->status_is(200) + ->content_like(qr{62 km}) + ->content_like(qr{00:57 Stunden}) + ->content_like(qr{nach Fahrplan: 00:58}) ->content_like(qr{Bei Abfahrt: -00:01 Stunden}) ->content_like(qr{Bei Ankunft: -00:02 Stunden}); diff --git a/t/02-registration.t b/t/registration.t index 799022f..ec09eaf 100644 --- a/t/02-registration.t +++ b/t/registration.t @@ -17,6 +17,8 @@ require "$FindBin::Bin/../index.pl"; my $t = Test::Mojo->new('Travelynx'); +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + if ( not $t->app->config->{db} ) { plan( skip_all => 'No database configured' ); } @@ -206,7 +208,8 @@ $res = $t->app->pg->db->select( 'pending_passwords', ['token'], { user_id => $uid } ); $token = $res->hash->{token}; -$t->get_ok("/recover/${uid}/${token}")->status_is(200) +$t->get_ok("/recover/${uid}/${token}") + ->status_is(200) ->content_like(qr{Neues Passwort eintragen}); $t->post_ok( diff --git a/t/21-relations.t b/t/relations.t index 857d20d..c5fbe32 100644 --- a/t/21-relations.t +++ b/t/relations.t @@ -17,6 +17,8 @@ require "$FindBin::Bin/../index.pl"; my $t = Test::Mojo->new('Travelynx'); +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + if ( not $t->app->config->{db} ) { plan( skip_all => 'No database configured' ); } diff --git a/t/01-static.t b/t/static-de.t index 3727f1e..21d0350 100644 --- a/t/01-static.t +++ b/t/static-de.t @@ -15,20 +15,28 @@ require "$FindBin::Bin/../index.pl"; my $t = Test::Mojo->new('Travelynx'); +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + $t->get_ok('/')->status_is(200); $t->text_like( 'a[href="/register"]' => qr{Registrieren} ); $t->text_like( 'a[href="/login"]' => qr{Anmelden} ); -$t->get_ok('/register')->status_is(200); -$t->element_exists('input[name="csrf_token"]'); -$t->element_exists('a[href="/impressum"]'); -$t->text_like( 'button' => qr{Registrieren} ); +$t->get_ok('/about')->status_is(200); +$t->get_ok('/api')->status_is(200); +$t->get_ok('/changelog')->status_is(200); +$t->get_ok('/legend')->status_is(200); +$t->get_ok('/offline.html')->status_is(200); $t->get_ok('/login')->status_is(200); $t->element_exists('input[name="csrf_token"]'); $t->text_like( 'button' => qr{Anmelden} ); -$t->get_ok('/about')->status_is(200); +$t->get_ok('/recover')->status_is(200); + +$t->get_ok('/register')->status_is(200); +$t->element_exists('input[name="csrf_token"]'); +$t->element_exists('a[href="/impressum"]'); +$t->text_like( 'button' => qr{Registrieren} ); # Protected sites should redirect to login form diff --git a/t/static-en.t b/t/static-en.t new file mode 100644 index 0000000..26641cb --- /dev/null +++ b/t/static-en.t @@ -0,0 +1,50 @@ +#!/usr/bin/env perl + +# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org> +# +# SPDX-License-Identifier: MIT + +use Mojo::Base -strict; + +use Test::More; +use Test::Mojo; + +# Include application +use FindBin; +require "$FindBin::Bin/../index.pl"; + +my $t = Test::Mojo->new('Travelynx'); + +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('en-GB') } ); + +$t->get_ok('/')->status_is(200); +$t->text_like( 'a[href="/register"]' => qr{Register} ); +$t->text_like( 'a[href="/login"]' => qr{Login} ); + +$t->get_ok('/about')->status_is(200); +$t->get_ok('/api')->status_is(200); +$t->get_ok('/changelog')->status_is(200); +$t->get_ok('/legend')->status_is(200); +$t->get_ok('/offline.html')->status_is(200); + +$t->get_ok('/login')->status_is(200); +$t->element_exists('input[name="csrf_token"]'); +$t->text_like( 'button' => qr{Login} ); + +$t->get_ok('/recover')->status_is(200); + +$t->get_ok('/register')->status_is(200); +$t->element_exists('input[name="csrf_token"]'); +$t->element_exists('a[href="/impressum"]'); +$t->text_like( 'button' => qr{Register} ); + +# Protected sites should redirect to login form + +for my $protected (qw(/account /account/password /history /s/EE)) { + $t->get_ok($protected)->text_like( 'button' => qr{Login} ); +} + +# Otherwise, we expect a 404 +$t->get_ok('/definitelydoesnotexist')->status_is(404); + +done_testing(); diff --git a/t/static-fr.t b/t/static-fr.t new file mode 100644 index 0000000..dc4c278 --- /dev/null +++ b/t/static-fr.t @@ -0,0 +1,50 @@ +#!/usr/bin/env perl + +# Copyright (C) 2020 Birte Kristina Friesel <derf@finalrewind.org> +# +# SPDX-License-Identifier: MIT + +use Mojo::Base -strict; + +use Test::More; +use Test::Mojo; + +# Include application +use FindBin; +require "$FindBin::Bin/../index.pl"; + +my $t = Test::Mojo->new('Travelynx'); + +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('fr-FR') } ); + +$t->get_ok('/')->status_is(200); +$t->text_like( 'a[href="/register"]' => qr{Inscription} ); +$t->text_like( 'a[href="/login"]' => qr{Connexion} ); + +$t->get_ok('/about')->status_is(200); +$t->get_ok('/api')->status_is(200); +$t->get_ok('/changelog')->status_is(200); +$t->get_ok('/legend')->status_is(200); +$t->get_ok('/offline.html')->status_is(200); + +$t->get_ok('/login')->status_is(200); +$t->element_exists('input[name="csrf_token"]'); +$t->text_like( 'button' => qr{Connexion} ); + +$t->get_ok('/recover')->status_is(200); + +$t->get_ok('/register')->status_is(200); +$t->element_exists('input[name="csrf_token"]'); +$t->element_exists('a[href="/impressum"]'); +$t->text_like( 'button' => qr{Inscription} ); + +# Protected sites should redirect to login form + +for my $protected (qw(/account /account/password /history /s/EE)) { + $t->get_ok($protected)->text_like( 'button' => qr{Connexion} ); +} + +# Otherwise, we expect a 404 +$t->get_ok('/definitelydoesnotexist')->status_is(404); + +done_testing(); diff --git a/t/22-transit-visibility.t b/t/transit-visibility.t index 8a68f5c..6aa48ed 100644 --- a/t/22-transit-visibility.t +++ b/t/transit-visibility.t @@ -20,6 +20,8 @@ require "$FindBin::Bin/../index.pl"; my $t = Test::Mojo->new('Travelynx'); +$t->ua->on( start => sub { $_[1]->req->headers->accept_language('de-DE') } ); + if ( not $t->app->config->{db} ) { plan( skip_all => 'No database configured' ); } @@ -108,30 +110,40 @@ sub test_intransit_visibility { $opt{effective_visibility_str}, $desc ); if ( $opt{public} ) { - $t->get_ok('/status/test1')->status_is(200)->content_like(qr{DPN\s*667}); - $t->get_ok('/ajax/status/test1.html')->status_is(200) + $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\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) + $t->get_ok('/status/test1') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok('/ajax/status/test1.html')->status_is(200) + $t->get_ok('/ajax/status/test1.html') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); } if ( $opt{with_token} ) { - $t->get_ok("/status/test1/$token")->status_is(200) + $t->get_ok("/status/test1/$token") + ->status_is(200) ->content_like(qr{DPN\s*667}); - $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200) + $t->get_ok("/ajax/status/test1.html?token=$j_token") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/status/test1/$token")->status_is(200) + $t->get_ok("/status/test1/$token") + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200) + $t->get_ok("/ajax/status/test1.html?token=$j_token") + ->status_is(200) ->content_like(qr{nicht eingecheckt}); } @@ -142,31 +154,41 @@ 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\s*667}); - $t->get_ok('/ajax/status/test1.html')->status_is(200) + $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\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) + $t->get_ok('/status/test1') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok('/ajax/status/test1.html')->status_is(200) + $t->get_ok('/ajax/status/test1.html') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); } # 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) + $t->get_ok("/status/test1/$token") + ->status_is(200) ->content_like(qr{DPN\s*667}); - $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200) + $t->get_ok("/ajax/status/test1.html?token=$j_token") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/status/test1/$token")->status_is(200) + $t->get_ok("/status/test1/$token") + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200) + $t->get_ok("/ajax/status/test1.html?token=$j_token") + ->status_is(200) ->content_like(qr{nicht eingecheckt}); } @@ -178,31 +200,41 @@ 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\s*667}); - $t->get_ok('/ajax/status/test1.html')->status_is(200) + $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\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) + $t->get_ok('/status/test1') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok('/ajax/status/test1.html')->status_is(200) + $t->get_ok('/ajax/status/test1.html') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); } # uid2 can see uid1 with token if visibility is >= unlisted if ( $opt{effective_visibility} >= 30 ) { - $t->get_ok("/status/test1/$token")->status_is(200) + $t->get_ok("/status/test1/$token") + ->status_is(200) ->content_like(qr{DPN\s*667}); - $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200) + $t->get_ok("/ajax/status/test1.html?token=$j_token") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/status/test1/$token")->status_is(200) + $t->get_ok("/status/test1/$token") + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200) + $t->get_ok("/ajax/status/test1.html?token=$j_token") + ->status_is(200) ->content_like(qr{nicht eingecheckt}); } @@ -214,31 +246,41 @@ 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\s*667}); - $t->get_ok('/ajax/status/test1.html')->status_is(200) + $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\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) + $t->get_ok('/status/test1') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok('/ajax/status/test1.html')->status_is(200) + $t->get_ok('/ajax/status/test1.html') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok('/p/test1')->status_is(200) + $t->get_ok('/p/test1') + ->status_is(200) ->content_like(qr{nicht eingecheckt}); } # uid3 can see uid1 with token if visibility is >= unlisted if ( $opt{effective_visibility} >= 30 ) { - $t->get_ok("/status/test1/$token")->status_is(200) + $t->get_ok("/status/test1/$token") + ->status_is(200) ->content_like(qr{DPN\s*667}); - $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200) + $t->get_ok("/ajax/status/test1.html?token=$j_token") + ->status_is(200) ->content_like(qr{DPN\s*667}); } else { - $t->get_ok("/status/test1/$token")->status_is(200) + $t->get_ok("/status/test1/$token") + ->status_is(200) ->content_like(qr{nicht eingecheckt}); - $t->get_ok("/ajax/status/test1.html?token=$j_token")->status_is(200) + $t->get_ok("/ajax/status/test1.html?token=$j_token") + ->status_is(200) ->content_like(qr{nicht eingecheckt}); } diff --git a/templates/_cancelled_departure.html.ep b/templates/_cancelled_departure.html.ep index 79492a5..db6cc5c 100644 --- a/templates/_cancelled_departure.html.ep +++ b/templates/_cancelled_departure.html.ep @@ -1,9 +1,9 @@ <div class="card"> <div class="card-content"> - <span class="card-title">Zugausfall</span> + <span class="card-title">Fahrtausfall</span> <p>Die Abfahrt von <%= $journey->{train_type} %> <%= $journey->{train_no} %> in <a href="/s/<%= $journey->{dep_eva} %>"><%= $journey->{dep_name} %></a> - entfällt. Der Zugausfall auf der Fahrt nach <%= $journey->{arr_name} %> wurde bereits dokumentiert. + entfällt. Der Ausfall der Fahrt nach <%= $journey->{arr_name} %> wurde bereits dokumentiert. </p> % if (my @connections = @{stash('connections_iris') // []}) { <p>Alternative Reisemöglichkeiten:</p> diff --git a/templates/_checked_in.html.ep b/templates/_checked_in.html.ep index 988bf39..69754e3 100644 --- a/templates/_checked_in.html.ep +++ b/templates/_checked_in.html.ep @@ -4,7 +4,7 @@ <div class="card-content"> <i class="material-icons right sync-failed-marker grey-text" style="display: none;">sync_problem</i> % if (not $journey->{arr_name}) { - <span class="card-title center-align">Ziel wählen</span> + <span class="card-title center-align"><%= L('status.select-destination') %></span> % } <span class="card-title center-align"> %= include '_format_train', journey => $journey @@ -19,26 +19,26 @@ data-dest="<%= $journey->{arr_name} %>" > % if ($journey->{boarding_countdown} > 60) { - Einfahrt in <%= journeys->min_to_human(int($journey->{boarding_countdown} / 60)) %><br/> + <%= L('status.boarding-in.pre') %> <%= journeys->min_to_human(int($journey->{boarding_countdown} / 60)) %> <%= L('status.boarding-in.post') %><br/> % } % elsif ($journey->{boarding_countdown} > 0) { - Fährt ein<br/> + <%= L('status.boarding-soon') %><br/> % } % if ($journey->{departure_countdown} > 60) { - Abfahrt in <%= journeys->min_to_human(int($journey->{departure_countdown} / 60)) %> + <%= L('status.departure-in.pre') %> <%= journeys->min_to_human(int($journey->{departure_countdown} / 60)) %> <%= L('status.departure-in.post') %> % } % elsif ($journey->{departure_countdown} > 0) { - Abfahrt in weniger als einer Minute + %= L('status.departure-soon') % } % elsif (defined $journey->{arrival_countdown}) { % if ($journey->{arrival_countdown} > 60) { - Ankunft in <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %> + <%= L('status.arrival-in.pre') %> <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %> <%= L('status.arrival-in.post') %> % } % elsif ($journey->{arrival_countdown} > 0) { - Ankunft in weniger als einer Minute + %= L('status.arrival-soon') % } % else { - Ziel erreicht + %= L('status.arrived') % } % if ($journey->{arrival_countdown} < (60 * 15) and $journey->{arr_platform}) { % if ($journey->{arr_direction} and $journey->{arr_direction} eq 'r') { @@ -48,7 +48,7 @@ <br/>◀ <%= $journey->{platform_type} %> <%= $journey->{arr_platform} %> % } % else { - <br/>auf <%= $journey->{platform_type} %> <%= $journey->{arr_platform} %> + <br/><%= L('status.arrive-on.pre') %> <%= $journey->{platform_type} %> <%= $journey->{arr_platform} %> <%= L('status.arrive-on.post') %> % } % } % } @@ -63,7 +63,7 @@ <br/>◀ <%= $journey->{platform_type} %> <%= $journey->{dep_platform} %> % } % else { - <br/>von <%= $journey->{platform_type} %> <%= $journey->{dep_platform} %> + <br/><%= L('status.depart-from.pre') %> <%= $journey->{platform_type} %> <%= $journey->{dep_platform} %> <%= L('status.depart-from.post') %> % } % } % if (my $wr = $journey->{wagonorder}) { @@ -202,8 +202,11 @@ % 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}) { - <li><i class="material-icons tiny">gps_off</i> Keine Echtzeitdaten vorhanden + % 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> <%= L('status.realtime-unavailable') %> % } % for my $message (reverse @{$journey->{messages} // []}) { % if ($journey->{sched_departure}->epoch - $message->[0]->epoch < 1800) { @@ -232,8 +235,7 @@ % } % if (defined $journey->{arrival_countdown} and $journey->{arrival_countdown} <= 0) { <p style="margin-top: 2ex;"> - Der automatische Checkout erfolgt wegen teilweise langsamer - Echtzeitdatenupdates erst etwa zehn Minuten nach der Ankunft. + %= L('status.delayed-auto-checkout') </p> % } % elsif (not $journey->{arr_name}) { @@ -272,7 +274,7 @@ % } % else { <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> Rückgängig + <i class="material-icons left" aria-hidden="true">undo</i> <%= L('status.undo') %> </a> % } % if (defined $journey->{arrival_countdown} and $journey->{arrival_countdown} <= 0) { @@ -281,7 +283,7 @@ style="margin-right: 0;" data-station="<%= $journey->{arr_name}%>"> <i class="material-icons left">done</i> - Auschecken + %= L('status.check-out') </a> % } % elsif ($journey->{arr_name}) { @@ -308,12 +310,12 @@ data-url="<%= url_for('/status')->to_abs->scheme('https') %>/<%= $user->{name} %>/<%= $journey->{sched_departure}->epoch %>?token=<%= $journey->{dep_eva} %>-<%= $journey->{timestamp}->epoch % 337 %>" % } > - <i class="material-icons left" aria-hidden="true">share</i> Teilen + <i class="material-icons left" aria-hidden="true">share</i> <%= L('status.share') %> </a> % } % else { <a class="right" href="/journey/visibility"> - <i class="material-icons left"><%= visibility_icon($journey_visibility) %></i> Sichtbarkeit + <i class="material-icons left"><%= visibility_icon($journey_visibility) %></i> <%= L('status.privacy-level') %> </a> % } </div> @@ -325,7 +327,7 @@ <div class="card" style="margin-top: <%= scalar @{stash('timeline') // []} ? '1.5rem' : '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">Meldungen</span> + <span class="card-title"><%= L('status.messages') %></span> % if (@{$journey->{extra_data}{him_msg} // []}) { <p style="margin-bottom: 2ex;"> <ul> @@ -356,55 +358,69 @@ <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">Karte</span> + <span class="card-title"><%= L('status.map') %></span> <div id="map" style="height: 70vh;"> </div> - %= include '_map', with_map_header => 0, station_coordinates => stash('station_coordinates'), polyline_groups => stash('polyline_groups') + %= include '_map', with_map_header => 0, station_coordinates => stash('station_coordinates'), polyline_groups => stash('polyline_groups'), markers => stash('markers') </div> </div> - <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> + % 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> <%= L('status.undo-checkin') %> </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> - % } </div> </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> + % } + % 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"><%= L('status.change-destination') %></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> + % } + </div> + </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> <%= L('status.undo-checkin') %> + </a> + </div> </div> - </div> + % } <p> - Falls das Backend ausgefallen ist oder die Fahrt aus anderen - Gründen verloren ging: + %= L('status.force-checkout-lead') </p> <p class="center-align"> <a class="action-checkout waves-light btn" data-force="1" data-station="<%= $journey->{arr_name} - %>">Ohne Echtzeitdaten auschecken</a> + %>"><%= L('status.force-checkout') %></a> </p> % } </div> diff --git a/templates/_format_train.html.ep b/templates/_format_train.html.ep index e82f3f9..cb81211 100644 --- a/templates/_format_train.html.ep +++ b/templates/_format_train.html.ep @@ -1,7 +1,7 @@ % if ($journey->{extra_data}{wagonorder_pride}) { 🏳️🌈 % } -<span class="dep-line <%= $journey->{train_type} // q{} %>"> +<span class="dep-line <%= ($journey->{train_type} // q{}) =~ tr{a-zA-Z_-}{}cdr %>"> % if (not $journey->{is_motis}) { <%= $journey->{train_type} %> % } diff --git a/templates/_history_stats.html.ep b/templates/_history_stats.html.ep index cbdbb13..e24e699 100644 --- a/templates/_history_stats.html.ep +++ b/templates/_history_stats.html.ep @@ -2,22 +2,23 @@ <div class="col s12"> <table class="striped"> <tr> - <th scope="row">Fahrten</th> + <th scope="row"><%= L('stats.trips') %></th> <td><%= $stats->{num_trains} %></td> </tr> <tr> - <th scope="row">Entfernung</th> - <td>ca. <%= sprintf('%.f', $stats->{km_route}) %> km - (Luftlinie: <%= sprintf('%.f', $stats->{km_beeline}) %> km)</td> + <th scope="row"><%= L('stats.distance') %></th> + <td>≈ <%= sprintf('%.f', $stats->{km_route}) %> km + <%= L('journey.beeline.pre') %><%= sprintf('%.f', $stats->{km_beeline}) %> km<%= L('journey.beeline.post') %></td> </tr> <tr> - <th scope="row">Fahrtzeit</th> - <td><%= $stats->{min_travel_real_strf} %> Stunden - (nach Fahrplan: <%= $stats->{min_travel_sched_strf} %>)</td> + <th scope="row"><%= L('stats.time') %></th> + <td><%= $stats->{min_travel_real_strf} %> <%= L('stats.hours') %> + (<%= L('stats.per-schedule') %>: <%= $stats->{min_travel_sched_strf} %>)</td> </tr> <tr> - <th scope="row">Wartezeit (nur Umstiege)</th> - <td><%= $stats->{min_interchange_real_strf} %> Stunden + <th scope="row"><%= L('stats.transfer-time') %></th> + <td><%= $stats->{min_interchange_real_strf} %> <%= L('stats.hours') %> + <!-- % if (@{$stats->{inconsistencies}}) { <br/><br/>Für Wartezeitberechnung nicht berücksichtigte Fahrten:<br/> % for my $field (@{$stats->{inconsistencies}}) { @@ -25,12 +26,13 @@ (Konflikt: <a href="/journey/<%= $field->{conflict}{id} %>"><%= $field->{conflict}{train} %> an <%= $field->{conflict}{arr} %></a>)<br/> % } % } + --> </td> </tr> <tr> - <th scope="row">Kumulierte Verspätung</th> - <td>Bei Abfahrt: <%= $stats->{delay_dep_strf} %> Stunden<br/> - Bei Ankunft: <%= $stats->{delay_arr_strf} %> Stunden</td> + <th scope="row"><%= L('stats.total-delay') %></th> + <td><%= L('stats.on-dep') %>: <%= $stats->{delay_dep_strf} %> <%= L('stats.hours') %><br/> + <%= L('stats.on-arr') %>: <%= $stats->{delay_arr_strf} %> <%= L('stats.hours') %></td> </tr> </table> </div> diff --git a/templates/_history_trains.html.ep b/templates/_history_trains.html.ep index 7ae2a1d..166d74d 100644 --- a/templates/_history_trains.html.ep +++ b/templates/_history_trains.html.ep @@ -16,8 +16,8 @@ % } <li class="collection-item"> <a href="<%= $detail_link %>"> - <span class="dep-line <%= $travel->{type} // q{} %>"> - % if (not $travel->{is_motis}) { + <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}%> @@ -37,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) %>) % } % } % } @@ -55,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 223bd68..93f116a 100644 --- a/templates/_map.html.ep +++ b/templates/_map.html.ep @@ -39,6 +39,15 @@ var pl; % } % } +% for my $marker (@{stash('markers') // []}) { + % if ($marker->[0] and $marker->[0][0] and $marker->[1]) { + { + const marker = L.marker([<%= $marker->[0][0] %>, <%= $marker->[0][1] %>]).addTo(map); + marker.bindPopup('<%= $marker->[1] %>'); + } + % } +% } + % if (my $b = stash('bounds')) { map.fitBounds([[<%= $b->[0][0] %>,<%= $b->[0][1] %>],[<%= $b->[1][0] %>,<%= $b->[1][1] %>]]); % } @@ -48,8 +57,8 @@ for (var station_id in stations) { color: '#f03', opacity: 0.7, fillColor: '#f03', - fillOpacity: 0.5, - radius: 250 + fillOpacity: 0.2, + radius: 200 }).bindPopup(stations[station_id][1]).addTo(map); } diff --git a/templates/_public_status_card.html.ep b/templates/_public_status_card.html.ep index 32b193a..ca5ddf8 100644 --- a/templates/_public_status_card.html.ep +++ b/templates/_public_status_card.html.ep @@ -11,7 +11,7 @@ <a href="/status/<%= $name %>"><%= $name %></a>: <%= include '_format_train', journey => $journey %> % } % else { - <a href="/p/<%= $name %>"><%= $name %></a> ist unterwegs + <a href="/p/<%= $name %>"><%= $name %></a> <%= L('status.is-checked-in') %> % } <i class="material-icons right"><%= visibility_icon($journey->{effective_visibility_str}) %></i> % if (not $journey->{extra_data}{rt}) { @@ -41,10 +41,10 @@ % } % elsif (defined $journey->{arrival_countdown}) { % if ($journey->{arrival_countdown} > 60) { - Ankunft in <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %> + <%= L('status.arrival-in.pre') %> <%= journeys->min_to_human(int($journey->{arrival_countdown} / 60)) %> <%= L('status.arrival-in.post') %> % } % elsif ($journey->{arrival_countdown} > 0) { - Ankunft in weniger als einer Minute + %= L('status.arrival-soon') % } % else { Ziel erreicht @@ -54,7 +54,7 @@ % } % } % elsif ($journey->{arr_name}) { - Ankunft in mehr als zwei Stunden + %= L('status.arrival-unknown') % } </div> <div class="progress" style="height: 1ex;"> @@ -216,14 +216,14 @@ % } % else { <div class="wagons" style="margin-top: 2ex;"> - Wagen:<br/> + <%= L('status.carriages') %>:<br/> %= include '_wagons', wagongroups => $journey->{wagongroups}; </div> % } % } % if (not stash('from_timeline')) { <div style="margin-top: 2ex;"> - Route:<br/> + <%= L('status.route') %>:<br/> % my $before = 1; % my $within = 0; % my $at_startstop = 0; @@ -280,7 +280,7 @@ <span class="card-title">Aktuell nicht eingecheckt</span> % } % else { - <span class="card-title"><a href="/p/<%= $name %>"><%= $name %></a> ist gerade nicht eingecheckt</span> + <span class="card-title"><a href="/p/<%= $name %>"><%= $name %></a> <%= L('status.is-not-checked-in') %></span> % } <div> % if ($journey->{arr_name}) { diff --git a/templates/_suggestions_dbris.html.ep b/templates/_suggestions_dbris.html.ep new file mode 100644 index 0000000..175a57b --- /dev/null +++ b/templates/_suggestions_dbris.html.ep @@ -0,0 +1,54 @@ +<ul class="collection departures connections"> + % for my $res (@{$suggestions}) { + % my ($dep, $dest) = @{$res}; + % my $row_class = ''; + % my $link_class = 'action-checkin'; + % if ($dep->is_cancelled) { + % $row_class = 'cancelled'; + % $link_class = 'action-cancelled-from'; + % } + % if ($checkin_from) { + <li class="collection-item <%= $row_class %> <%= $link_class %>" + data-dbris="<%= $dbris %>" + data-station="<%= $dep->stop_eva %>" + data-train="<%= $dep->id %>" + data-suffix="<%= $dep->maybe_line_no %>" + data-ts="<%= ($dep->sched_dep // $dep->dep)->epoch %>" + data-dest="<%= $dest->{name} %>"> + % } + % else { + <li class="collection-item <%= $row_class %>"> + % } + <a class="dep-time" href="#"> + % if ($dep->is_cancelled) { + %= $dep->sched_dep->strftime('%H:%M') + % } + % else { + %= $dep->dep->strftime('%H:%M') + % } + % if ($dep->delay) { + %= sprintf('(%+d)', $dep->delay) + % } + </a> + <span class="connect-platform-wrapper"> + % if ($dep->platform) { + <span> + % if (($dep->type // q{}) =~ m{ ast | bus | ruf }ix) { + Steig + % } + % else { + Gleis + % } + %= $dep->platform + </span> + % } + <span class="dep-line <%= $dep->type // q{} %>"> + %= $dep->line + </span> + </span> + <span class="dep-dest"> + %= $dest->{name} + </span> + </li> + % } +</ul> diff --git a/templates/_suggestions_efa.html.ep b/templates/_suggestions_efa.html.ep new file mode 100644 index 0000000..68fb4a9 --- /dev/null +++ b/templates/_suggestions_efa.html.ep @@ -0,0 +1,53 @@ +<ul class="collection departures connections"> + % for my $res (@{$suggestions}) { + % my ($dep, $dest) = @{$res}; + % my $row_class = ''; + % my $link_class = 'action-checkin'; + % if ($dep->is_cancelled) { + % $row_class = 'cancelled'; + % $link_class = 'action-cancelled-from'; + % } + % if ($checkin_from) { + <li class="collection-item <%= $row_class %> <%= $link_class %>" + data-efa="<%= $efa %>" + data-station="<%= $dep->stop_id_num %>" + data-train="<%= $dep->id %>" + data-ts="<%= ($dep->sched_datetime // $dep->datetime)->epoch %>" + data-dest="<%= $dest->{name} %>"> + % } + % else { + <li class="collection-item <%= $row_class %>"> + % } + <a class="dep-time" href="#"> + % if ($dep->is_cancelled) { + %= $dep->sched_datetime->strftime('%H:%M') + % } + % else { + %= $dep->datetime->strftime('%H:%M') + % } + % if ($dep->delay) { + %= sprintf('(%+d)', $dep->delay) + % } + </a> + <span class="connect-platform-wrapper"> + % if ($dep->platform) { + <span> + % if (($dep->type // q{}) =~ m{ ast | bus | ruf }ix) { + Steig + % } + % else { + Gleis + % } + %= $dep->platform + </span> + % } + <span class="dep-line <%= ($dep->type // q{}) =~ tr{a-zA-Z_-}{}cdr %>"> + %= $dep->line + </span> + </span> + <span class="dep-dest"> + %= $dest->{name} + </span> + </li> + % } +</ul> diff --git a/templates/_wagons.html.ep b/templates/_wagons.html.ep index 4090f11..3f906c0 100644 --- a/templates/_wagons.html.ep +++ b/templates/_wagons.html.ep @@ -7,12 +7,12 @@ % elsif ($wagon_number and my $group_name = app->ice_name->{$wagon_number}) { „<%= $group_name %>“ % } - als <b><%= $wagongroup->{type} // $journey->{type} %> <%= $wagongroup->{no} %></b> + <%= L('wagons.name-as-type') %> <b><%= $wagongroup->{type} // $journey->{type} %> <%= $wagongroup->{no} %></b> % if ($wagongroup->{from}) { - von <b><%= $wagongroup->{from} %></b> + <%= L('wagons.from.pre') %> <b><%= $wagongroup->{from} %></b> <%= L('wagons.from.post') %> % } % if ($wagongroup->{to}) { - nach <b><%= $wagongroup->{to} %></b> + <%= L('wagons.to.pre') %> <b><%= $wagongroup->{to} %></b> <%= L('wagons.to.post') %> % } <br/> % for my $wagon (@{$wagongroup->{wagons}}) { @@ -24,7 +24,7 @@ % } %= $wagon->{type} % if ($wagon->{number}) { - – Wagen <%= $wagon->{number} %> + – <%= L('wagons.carriage') %> <%= $wagon->{number} %> % } <br/> % } diff --git a/templates/about.html.ep b/templates/about.html.ep index 3167d91..a76cb94 100644 --- a/templates/about.html.ep +++ b/templates/about.html.ep @@ -1,47 +1,42 @@ <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> - 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: + <%= L('about.developed-by.lead') %> <a href="https://finalrewind.org">derf</a> + <%= L('about.developed-by.and') %> <a href="https://github.com/derf/travelynx/graphs/contributors"><%= L('about.developed-by.others') %></a> <%= L('about.developed-by.tail') %> <%= L('about.developed-by.tail') %><br/> + <a href="<%= app->config->{ref}{source} // 'https://git.finalrewind.org/travelynx' %>"><%= L('about.source-code') %></a> <%= L('about.licence-agplv3') %><br/><br/> + <%= L('about.data-sources') %>: <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 %> und + 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 %><br/> - Haltestellendaten - © DB Station&Service AG, - Europaplatz 1, - 10557 Berlin, lizensiert unter CC-BY 4.0 + v<%= $Travel::Status::DE::IRIS::VERSION %><%= L('about.data-sources.last-and') %> + <a href="https://finalrewind.org/projects/Travel-Status-MOTIS/">Travel::Status::MOTIS</a> + v<%= $Travel::Status::MOTIS::VERSION %><br/> </div> </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. + %= L('about.disclaimer') </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> + <a href="https://social.skyshaper.org/derf" class="waves-effect waves-light btn"><i class="material-icons left">message</i><%= L('about.contact') %></a> </div> <div class="col s12 m12 l4 center-align" style="margin-top: 1em;"> % if (my $issue_url = app->config->{ref}{issues}) { - <a href="<%= $issue_url %>" class="waves-effect waves-light btn"><i class="material-icons left">bug_report</i>Bugs?</a> + <a href="<%= $issue_url %>" class="waves-effect waves-light btn"><i class="material-icons left">bug_report</i><%= L('about.bugs') %></a> % } </div> <div class="col s12 m12 l4 center-align" style="margin-top: 1em;"> - <a href="/changelog" class="waves-effect waves-light btn"><i class="material-icons left">list</i>Changelog</a> + <a href="/changelog" class="waves-effect waves-light btn"><i class="material-icons left">list</i><%= L('about.changelog') %></a> </div> </div> diff --git a/templates/account.html.ep b/templates/account.html.ep index e4bf38d..17f708f 100644 --- a/templates/account.html.ep +++ b/templates/account.html.ep @@ -8,31 +8,34 @@ <div class="card success-color"> <div class="card-content white-text"> % if ($success eq 'name') { - <span class="card-title">Name geändert</span> + <span class="card-title"><%= L('account.changed-name') %></span> % } % elsif ($success eq 'mail') { - <span class="card-title">Mail-Adresse geändert</span> + <span class="card-title"><%= L('account.changed-mail') %></span> % } % elsif ($success eq 'password') { - <span class="card-title">Passwort geändert</span> + <span class="card-title"><%= L('account.changed-password') %></span> + % } + % elsif ($success eq 'language') { + <span class="card-title"><%= L('account.changed-language') %></span> % } % elsif ($success eq 'privacy') { - <span class="card-title">Einstellungen zu öffentlichen Account-Daten geändert</span> + <span class="card-title"><%= L('account.changed-privacy') %></span> % } % elsif ($success eq 'social') { - <span class="card-title">Einstellungen zur Interaktionen mit anderen Accounts geändert</span> + <span class="card-title"><%= L('account.changed-social') %></span> % } % elsif ($success eq 'traewelling') { - <span class="card-title">Träwelling-Verknüpfung aktualisiert</span> + <span class="card-title"><%= L('account.changed-traewelling') %></span> % } % elsif ($success eq 'use_history') { - <span class="card-title">Einstellungen zu vorgeschlagenen Verbindungen geändert</span> + <span class="card-title"><%= L('account.changed-history') %></span> % } % elsif ($success eq 'webhook') { - <span class="card-title">Web Hook aktualisiert</span> + <span class="card-title"><%= L('account.changed-webhook') %></span> % } % elsif ($success eq 'clear_notifications') { - <span class="card-title">Benachrichtigungen gelesen</span> + <span class="card-title"><%= L('account.cleared-notifications') %></span> % } </div> </div> @@ -46,34 +49,38 @@ % my $use_history = users->use_history(uid => $acc->{id}); <div class="row"> <div class="col s12"> - <h2>Account</h2> + <h2><%= L('account.account') %></h2> <table class="striped"> <tr> - <th scope="row">Name</th> + <th scope="row"><%= L('account.name') %></th> <td><a href="/account/name"><i class="material-icons">edit</i></a><%= $acc->{name} %></td> </tr> <tr> - <th scope="row">Mail</th> + <th scope="row"><%= L('account.mail') %></th> <td><a href="/account/mail"><i class="material-icons">edit</i></a><%= $acc->{email} %></td> </tr> <tr> - <th scope="row">Passwort</th> + <th scope="row"><%= L('account.password') %></th> <td><a href="/account/password"><i class="material-icons">edit</i></a></td> </tr> <tr> - <th scope="row">Verbindungen</th> + <th scope="row"><%= L('account.language') %></th> + <td><a href="/account/language"><i class="material-icons">edit</i></a><%= $acc->{languages}[0] // q{} %></td> + </tr> + <tr> + <th scope="row"><%= L('account.connections') %></th> <td> <a href="/account/insight"><i class="material-icons">edit</i></a> % if ($use_history & 0x03) { - Vorschläge aktiv + %= L('account.connections.enabled') % } % else { - <span style="color: #999999;">Vorschläge deaktiviert</span> + <span style="color: #999999;"><%= L('account.connections.disabled') %></span> % } </td> </tr> <tr> - <th scope="row">Sichtbarkeit</th> + <th scope="row"><%= L('account.visibility') %></th> <td> <a href="/account/privacy"><i class="material-icons">edit</i></a> <i class="material-icons">check</i><i class="material-icons"><%= visibility_icon($acc->{default_visibility_str}) %></i> @@ -81,41 +88,41 @@ </td> </tr> <tr> - <th scope="row">Interaktion</th> + <th scope="row"><%= L('account.interaction') %></th> <td> <a href="/account/social"><i class="material-icons">edit</i></a> % if ($acc->{accept_follows}) { - <span>Accounts können dir direkt folgen</span> + <span><%= L('account.interaction.accept-follows') %></span> % } % elsif ($acc->{accept_follow_requests}) { - <span>Accounts können dir auf Anfrage folgen + <span><%= L('account.interaction.accept-follow-requests') %> % if ($num_rx_follow_requests == 1) { - – <a href="/account/social/follow-requests-received"><strong>eine</strong> offene Anfrage</a> + – <a href="/account/social/follow-requests-received"><strong><%= L('account.interaction.one') %></strong> <%= L('account.interaction.open-request') %></a> % } elsif ($num_rx_follow_requests) { - – <a href="/account/social/follow-requests-received"><strong><%= $num_rx_follow_requests %></strong> offene Anfragen</a> + – <a href="/account/social/follow-requests-received"><strong><%= $num_rx_follow_requests %></strong> <%= L('account.interaction.open-requests') %></a> % } </span> % } % else { - <span style="color: #999999;">Accounts können dir nicht folgen</span> + <span style="color: #999999;"><%= L('account.interaction.disabled') %></span> % } </td> </tr> <tr> - <th scope="row">Web Hook</th> + <th scope="row"><%= L('account.webhook') %></th> <td> <a href="/account/hooks"><i class="material-icons">edit</i></a> % if (not $hook->{enabled}) { - <span style="color: #999999;">Nicht eingerichtet</span> + <span style="color: #999999;"><%= L('account.webhook.disabled') %></span> % } % elsif ($hook->{latest_run}->epoch == 0) { - Aktiv, noch nicht ausgeführt + <%= L('account.webhook.active-pending') %> % } % elsif ($hook->{errored}) { - Aktiv, fehlerhaft <i class="material-icons" aria-hidden="true">error</i> + <%= L('account.webhook.active-error') %> <i class="material-icons" aria-hidden="true">error</i> % } % else { - Aktiv + <%= L('account.webhook.active') %> % } </td> </tr> @@ -123,7 +130,7 @@ <tr> <th scope="row">Träwelling</th> <td> - Wird wegen Inkompatibilität zwischen bahn.de und transitous derzeit nicht unterstützt + <%= L('account.traewelling.unsupported') %> <!-- <a href="/account/traewelling"><i class="material-icons">edit</i></a> % if (not ($traewelling->{token})) { @@ -152,8 +159,8 @@ </tr> % } <tr> - <th scope="row">Registriert am</th> - <td><%= $acc->{registered_at}->strftime('%d.%m.%Y %H:%M') %></td> + <th scope="row"><%= L('account.registration-date') %></th> + <td><%= $acc->{registered_at}->strftime(L('strftime.datetime')) %></td> </tr> </table> </div> @@ -163,7 +170,7 @@ %= form_for 'logout' => begin %= csrf_field <button class="btn waves-effect waves-light" type="submit" name="action" value="logout"> - Abmelden + %= L('button.logout') </button> %= end </div> @@ -172,32 +179,32 @@ % if ($num_rx_follow_requests or $num_tx_follow_requests or $num_followers or $num_following or $num_blocked) { <div class="row"> <div class="col s12"> - <h2>Interaktion</h2> + <h2><%= L('account.interaction') %></h2> <p> - <a href="/p/<%= $acc->{name} %>">Öffentliches Profil</a> + <a href="/p/<%= $acc->{name} %>"><%= L('account.profile') %></a> </p> <table class="striped"> <tr> - <th scope="row">Anfragen</th> + <th scope="row"><%= L('account.interaction.requests') %></th> <td> % if ($num_rx_follow_requests == 0) { - <span style="color: #999999;">keine eingehend</span> + <span style="color: #999999;"><%= L('account.interaction.requests.incoming.none') %></span> % } % elsif ($num_rx_follow_requests == 1) { - <a href="/account/social/follow-requests-received"><strong>eine</strong> eingehend</a> + <a href="/account/social/follow-requests-received"><strong><%= L('account.interaction.one') %></strong> <%= L('account.interaction.requests.incoming.some') %></a> % } % else { - <a href="/account/social/follow-requests-received"><strong><%= $num_rx_follow_requests %></strong> eingehend</a> + <a href="/account/social/follow-requests-received"><strong><%= $num_rx_follow_requests %></strong> <%= L('account.interaction.requests.incoming.some') %></a> % } <br/> % if ($num_tx_follow_requests == 0) { - <span style="color: #999999;">keine ausgehend</span> + <span style="color: #999999;"><%= L('account.interaction.requests.outgoing.none') %></span> % } % elsif ($num_tx_follow_requests == 1) { - <a href="/account/social/follow-requests-sent"><strong>eine</strong> ausgehend</a> + <a href="/account/social/follow-requests-sent"><strong><%= L('account.interaction.one') %></strong> <%= L('account.interaction.requests.outgoing.some') %></a> % } % else { - <a href="/account/social/follow-requests-sent"><strong><%= $num_tx_follow_requests %></strong> ausgehend</a> + <a href="/account/social/follow-requests-sent"><strong><%= $num_tx_follow_requests %></strong> <%= L('account.interaction.requests.outgoing.some') %></a> % } </td> </tr> diff --git a/templates/add_intransit.html.ep b/templates/add_intransit.html.ep new file mode 100644 index 0000000..a044917 --- /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 YYYY-MM-DDTHH:MM. Bei den Zwischenhalten kann auch nur HH:MM angegeben werden</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"> + %= datetime_field 'sched_departure', id => 'sched_departure', class => 'validate', required => undef + <label for="sched_departure" class="active">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"> + %= datetime_field 'sched_arrival', id => 'sched_arrival', class => 'validate', required => undef + <label for="sched_arrival" class="active">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> (Format siehe oben, ein ggf. ausgelassenes Datum wird ergänzt) + </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..94f9270 100644 --- a/templates/add_journey.html.ep +++ b/templates/add_journey.html.ep @@ -33,10 +33,22 @@ <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>Zeitangaben im Format YYYY-MM-DDTHH: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,35 +66,37 @@ <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]' - <label for="sched_departure">Geplante Abfahrt</label> + %= datetime_field 'sched_departure', id => 'sched_departure', class => 'validate', required => undef + <label for="sched_departure" class="active">Geplante Abfahrt</label> </div> <div class="input-field col s12"> - %= text_field 'rt_departure', id => 'rt_departure', class => 'validate', 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="rt_departure">Tatsächliche Abfahrt (wenn leer: pünktlich)</label> + %= datetime_field 'rt_departure', id => 'rt_departure', class => 'validate' + <label for="rt_departure" class="active">Tatsächliche Abfahrt (wenn leer: pünktlich)</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 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]' - <label for="sched_arrival">Geplante Ankunft</label> + %= datetime_field 'sched_arrival', id => 'sched_arrival', class => 'validate', required => undef + <label for="sched_arrival" class="active">Geplante Ankunft</label> </div> <div class="input-field col s12"> - %= text_field 'rt_arrival', id => 'rt_arrival', class => 'validate', 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="rt_arrival">Tatsächliche Ankunft (wenn leer: pünktlich)</label> + %= datetime_field 'rt_arrival', id => 'rt_arrival', class => 'validate' + <label for="rt_arrival" class="active">Tatsächliche Ankunft (wenn leer: pünktlich)</label> </div> </div> <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 099474c..6db9536 100644 --- a/templates/api_documentation.html.ep +++ b/templates/api_documentation.html.ep @@ -89,23 +89,25 @@ <div class="col s12"> <p> Checkin per API. Sobald eine Zielstation bekannt ist, erfolgt der - Checkout wie beim Webinterface automatisch zehn Minuten nach Ankunft. - Bitte beachten: Es wird nicht überprüft, ob die angegebene Zielstation - in der vorgesehenen Route der Fahrt vorkommt oder nicht. + Checkout wie beim Webinterface automatisch spätestens eine halbe + Stunde nach Ankunft. Bitte beachten: Es wird nicht überprüft, ob + die angegebene Zielstation in der vorgesehenen Route der Fahrt + vorkommt oder nicht. </p> <p> - Falls du zum Checkinzeitpunkt bereits in eine andere Fahrt eingecheckt - bist, wirst du zunächst am gewählten Startbahnhof aus diesem ausgecheckt. - Der Checkout erfolgt unabhängig davon, ob die vorherige Fahrt an dieser - Station verkehrt oder nicht. Falls nach einem Checkin ohne Zielwahl - innerhalb von 48 Stunden kein Zielbahnhof nachgetragen wird, wird der - Checkin automatisch rückgängig gemacht. + Falls du zum Checkinzeitpunkt bereits in eine andere Fahrt + eingecheckt bist, wirst du zunächst am gewählten Startbahnhof aus + diesem ausgecheckt. Der Checkout erfolgt unabhängig davon, ob die + vorherige Fahrt an dieser Station verkehrt oder nicht. Falls nach + einem Checkin ohne Zielwahl innerhalb von 48 Stunden kein + Zielbahnhof nachgetragen wird, wird der Checkin automatisch + rückgängig gemacht. </p> <p> Das Verhalten des Checkout-Endpunkts hängt vom Zeitpunkt ab. Wenn die Fahrt den angegebenen Zielbahnhof bereits erreicht hat, wird dort - ausgecheckt. Andernfalls wird das Reiseziel aktualisiert und etwa zehn - Minuten nach Ankunft automatisch ausgecheckt. + ausgecheckt. Andernfalls wird das Reiseziel aktualisiert und + spätestens eine halbe Stunde nach Ankunft automatisch ausgecheckt. </p> <p style="font-family: Monospace;"> curl -X POST -H "Content-Type: application/json" -d '{"token":"<%= $uid %>-<%= $token->{travel} // 'TOKEN' %>"}' <%= $api_root %>/travel diff --git a/templates/cancelled.html.ep b/templates/cancelled.html.ep index 4fab434..fe0b958 100644 --- a/templates/cancelled.html.ep +++ b/templates/cancelled.html.ep @@ -1,7 +1,7 @@ -<h1>Zugausfälle</h1> +<h1>Ausfälle</h1> <div class="row"> <div class="col s12"> - Die folgenden Zugfahrten haben nicht stattgefunden. + Die folgenden Fahrten haben nicht wie geplant stattgefunden. </div> </div> diff --git a/templates/changelog.html.ep b/templates/changelog.html.ep index dc454c5..042a46e 100644 --- a/templates/changelog.html.ep +++ b/templates/changelog.html.ep @@ -2,6 +2,67 @@ <div class="row"> <div class="col s12 m1 l1"> + 2.17 + </div> + <div class="col s12 m11 l11"> + <p> + <i class="material-icons left" aria-label="<%= L('changelog.added') %>">add</i> + %= L('changelog.2-17.1') + </p> + </div> +</div> + +<div class="row"> + <div class="col s12 m1 l1"> + 2.16 + </div> + <div class="col s12 m11 l11"> + <p> + <i class="material-icons left" aria-label="<%= L('changelog.added') %>">add</i> + %= L('changelog.2-16.1') + </p> + <p> + <i class="material-icons left" aria-label="<%= L('changelog.bugfix') %>">build</i> + %= L('changelog.2-16.2') + </p> + </div> +</div> + +<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"> diff --git a/templates/departures.html.ep b/templates/departures.html.ep index 16b5f3c..0d57039 100644 --- a/templates/departures.html.ep +++ b/templates/departures.html.ep @@ -81,14 +81,7 @@ </div> </div> % } -% elsif ($user_status->{timestamp_delta} < 180) { - <div class="row"> - <div class="col s12"> - %= include '_checked_out', journey => $user_status; - </div> - </div> -% } -% elsif (not param('train') and (@{stash('connections_iris') // []} or @{stash('connections_hafas') // []}) ) { +% elsif (not param('train') and (@{stash('connections_iris') // []} or @{stash('connections_hafas') // []} or @{stash('suggestions') // []}) ) { % $have_connections = 1; <div class="row"> <div class="col s12"> @@ -99,6 +92,21 @@ % if (@{stash('connections_hafas') // []}) { %= include '_connections_hafas', connections => stash('connections_hafas'), checkin_from => $eva; % } + % if (@{stash('suggestions') // []}) { + % if ($dbris) { + %= include '_suggestions_dbris', suggestions => stash('suggestions'), checkin_from => $eva; + % } + % elsif ($efa) { + %= include '_suggestions_efa', suggestions => stash('suggestions'), checkin_from => $eva; + % } + % } + </div> + </div> +% } +% if (not $user_status->{checked_in} and $user_status->{timestamp_delta} < 180) { + <div class="row"> + <div class="col s12"> + %= include '_checked_out', journey => $user_status; </div> </div> % } @@ -190,3 +198,11 @@ % } </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/edit_journey.html.ep b/templates/edit_journey.html.ep index cb867e5..57941c0 100644 --- a/templates/edit_journey.html.ep +++ b/templates/edit_journey.html.ep @@ -69,13 +69,13 @@ <tr> <th scope="row">Geplante Abfahrt</th> <td class="input-field"> - %= 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]' + %= 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](:[0-9][0-9])?' </td> </tr> <tr> <th scope="row">Tatsächliche Abfahrt</th> <td class="input-field"> - %= text_field 'rt_departure', id => 'real_departure', class => 'validate', 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]' + %= text_field 'rt_departure', id => 'real_departure', class => 'validate', 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](:[0-9][0-9])?' </td> </tr> <tr> @@ -87,13 +87,13 @@ <tr> <th scope="row">Geplante Ankunft</th> <td class="input-field"> - %= 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]' + %= 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](:[0-9][0-9])?' </td> </tr> <tr> <th scope="row">Tatsächliche Ankunft</th> <td class="input-field"> - %= text_field 'rt_arrival', id => 'real_arrival', class => 'validate', 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]' + %= text_field 'rt_arrival', id => 'real_arrival', class => 'validate', 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](:[0-9][0-9])?' </td> </tr> <tr> diff --git a/templates/history_by_month.html.ep b/templates/history_by_month.html.ep index c3b1004..dec4c8b 100644 --- a/templates/history_by_month.html.ep +++ b/templates/history_by_month.html.ep @@ -6,7 +6,7 @@ <div class="row"> <div class="col s12 m12 l12 center-align"> - <a href="/history/map?filter_from=<%= $filter_from->strftime('%d.%m.%Y') %>&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> + <a href="/history/map?filter_from=<%= $filter_from->strftime('%F') %>&filter_to=<%= $filter_to->strftime('%F') %>" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">map</i> Karte</a> </div> </div> diff --git a/templates/history_by_year.html.ep b/templates/history_by_year.html.ep index 6aa0c2d..1557b77 100644 --- a/templates/history_by_year.html.ep +++ b/templates/history_by_year.html.ep @@ -7,7 +7,7 @@ <div class="row"> % if (stash('have_review')) { <div class="col s12 m12 l5 center-align"> - <a href="/history/map?filter_from=1.1.<%= $year %>&filter_to=31.12.<%= $year %>" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">map</i> Karte</a> + <a href="/history/map?filter_from=<%= $year %>-01-01&filter_to=<%= $year %>-12-31" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">map</i> Karte</a> </div> <div class="col s12 m12 l2"> </div> <div class="col s12 m12 l5 center-align"> @@ -16,7 +16,7 @@ % } % else { <div class="col s12 m12 l12 center-align"> - <a href="/history/map?filter_from=1.1.<%= $year %>&filter_to=31.12.<%= $year %>" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">map</i> Karte</a> + <a href="/history/map?filter_from=<%= $year %>-01-01&filter_to=<%= $year %>-12-31" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">map</i> Karte</a> </div> % } </div> diff --git a/templates/history_map.html.ep b/templates/history_map.html.ep index 57ba81f..6620cb2 100644 --- a/templates/history_map.html.ep +++ b/templates/history_map.html.ep @@ -88,14 +88,14 @@ </p> <div class="row"> <div class="input-field col s12"> - %= text_field 'filter_from', id => 'filter_from', class => 'validate', 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="filter_from">Abfahrt ab (DD.MM.YYYY)</label> + %= date_field 'filter_from', id => 'filter_from', class => 'validate' + <label for="filter_from" class="active">Abfahrt ab</label> </div> </div> <div class="row"> <div class="input-field col s12"> - %= text_field 'filter_to', id => 'filter_to', class => 'validate', 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="filter_to">Abfahrt bis (DD.MM.YYYY)</label> + %= date_field 'filter_to', id => 'filter_to', class => 'validate' + <label for="filter_to" class="active">Abfahrt bis</label> </div> </div> <div class="row"> @@ -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 45a1598..09ad76d 100644 --- a/templates/journey.html.ep +++ b/templates/journey.html.ep @@ -3,8 +3,8 @@ <div class="col s12"> <div class="card caution-color"> <div class="card-content white-text"> - <span class="card-title">Fehler</span> - <p>Fahrt nicht gefunden.</p> + <span class="card-title"><%= L('header.error') %></span> + <p><%= L('journey.not-found') %></p> </div> </div> </div> @@ -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> @@ -67,9 +55,9 @@ %= end </div> % } - <table class="striped"> + <table class="striped journey"> <tr> - <th scope="row">Fahrt</th> + <th scope="row"><%= L('journey.trip') %></th> <td> <%= $journey->{type} %> <%= $journey->{no} %> % if ($journey->{line}) { @@ -78,19 +66,48 @@ </td> </tr> <tr> - <th scope="row">Abfahrt</th> + <th scope="row"><%= L('journey.from') %></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"><%= L('journey.to') %></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"><%= L('journey.departure') %></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}->epoch % 60) ? $journey->{rt_departure}->strftime('%H:%M:%S') : $journey->{rt_departure}->strftime('%H:%M'); - (<%= sprintf('%+d', ($journey->{rt_departure}->epoch - $journey->{sched_departure}->epoch) / 60) %>, - Plan: <%= ($journey->{sched_departure}->epoch % 60) ? $journey->{sched_departure}->strftime('%H:%M:%S') : $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}->epoch % 60) ? $journey->{sched_departure}->strftime('%H:%M:%S') : $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) { ∗ @@ -98,24 +115,29 @@ </td> </tr> <tr> - <th scope="row">Ankunft</th> + <th scope="row"><%= L('journey.arrival') %></th> <td> % 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}->epoch % 60) ? $journey->{rt_arrival}->strftime('%H:%M:%S') : $journey->{rt_arrival}->strftime('%H:%M'); - (<%= sprintf('%+d', ($journey->{rt_arrival}->epoch - $journey->{sched_arrival}->epoch) / 60) %>, - Plan: <%= ($journey->{sched_arrival}->epoch % 60) ? $journey->{sched_arrival}->strftime('%H:%M:%S') : $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}->epoch % 60) ? $journey->{sched_arrival}->strftime('%H:%M:%S') : $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) { ∗ @@ -123,7 +145,7 @@ </td> </tr> <tr> - <th scope="row">Strecke</th> + <th scope="row"><%= L('journey.distance') %></th> <td> % if ($journey->{skip_route}) { <i class="material-icons right">location_off</i> @@ -131,10 +153,10 @@ % } % if ($journey->{km_route} > 0.1) { ca. <%= sprintf_km($journey->{km_route}) %> - (Luftlinie: <%= sprintf_km($journey->{km_beeline}) %>) + <%= L('journey.beeline.pre') %><%= sprintf_km($journey->{km_beeline}) %><%= L('journey.beeline.post') %> % } % elsif ($journey->{km_beeline} > 0.1) { - (Luftlinie: <%= sprintf_km($journey->{km_beeline}) %>) + <%= L('journey.beeline.pre') %><%= sprintf_km($journey->{km_beeline}) %><%= L('journey.beeline.post') %> % } % else { ? @@ -145,7 +167,7 @@ </td> </tr> <tr> - <th scope="row">Tempo</th> + <th scope="row"><%= L('journey.speed') %></th> <td> % if ($journey->{skip_route}) { <i class="material-icons right">location_off</i> @@ -168,7 +190,7 @@ </tr> % if ($journey->{user_data}{operator} or scalar @{ $journey->{user_data}{operators} // [] }) { <tr> - <th scope="row">Betrieb</th> + <th scope="row"><%= L('journey.operator') %></th> <td> %= $journey->{user_data}{operator} // join(q{, }, @{$journey->{user_data}{operators}}) </td> @@ -176,7 +198,7 @@ % } % if ($journey->{messages} and @{$journey->{messages}}) { <tr> - <th scope="row">Meldungen</th> + <th scope="row"><%= L('journey.messages') %></th> <td> % for my $message (@{$journey->{messages} // []}) { % my ($ts, $msg) = @{$message}; @@ -187,7 +209,7 @@ % } % if ($journey->{user_data}{him_msg} and @{$journey->{user_data}{him_msg}}) { <tr> - <th scope="row">Meldungen</th> + <th scope="row"><%= L('journey.messages') %></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/> @@ -197,7 +219,7 @@ % } % if ($journey->{user_data} and $journey->{user_data}{comment}) { <tr> - <th scope="row">Kommentar</th> + <th scope="row"><%= L('journey.comment') %></th> <td> <%= $journey->{user_data}{comment} %> </td> @@ -205,7 +227,7 @@ % } % if ($journey->{user_data} and $journey->{user_data}{wagongroups} and not exists $journey->{user_data}{wagons}) { <tr> - <th scope="row">Rollmaterial</th> + <th scope="row"><%= L('journey.carriages') %></th> <td class="wagons"> %= include '_wagons', wagongroups => $journey->{user_data}{wagongroups}; </td> @@ -213,7 +235,7 @@ % } % elsif ($journey->{user_data} and $journey->{user_data}{wagons}) { <tr> - <th scope="row">Rollmaterial</th> + <th scope="row"><%= L('journey.carriages') %></th> <td class="wagons"> % for my $wagongroup (@{$journey->{user_data}{wagongroups} // []}) { Wagenverbund <%= $wagongroup %><br/> @@ -230,16 +252,17 @@ </tr> % } <tr> - <th scope="row">Route</th> - <td> + <th scope="row"><%= L('journey.route') %></th> + <td class="route"> % my $before = 1; % my $within = 0; % my $at_startstop = 0; + % my $i = 0; % for my $station (@{$journey->{route}}) { - % if (($station->[1] and $station->[1] == $journey->{from_eva}) or $station->[0] eq $journey->{from_name}) { + % if ($i == $journey->{route_dep_index}) { % $within = 1; $at_startstop = 1; % } - % elsif (($station->[1] and $station->[1] == $journey->{to_eva}) or $station->[0] eq $journey->{to_name}) { + % elsif ($i == $journey->{route_arr_index}) { % $within = 0; $at_startstop = 1; % } % else { @@ -247,10 +270,10 @@ % } <span style="color: #808080;"> % if ($before and $station->[2]{sched_dep}) { - %= $station->[2]{sched_dep}->strftime('%H:%M') + %= $station->[2]{sched_dep_dt}->strftime('%H:%M') % } % elsif (not $before and $station->[2]{sched_arr}) { - %= $station->[2]{sched_arr}->strftime('%H:%M') + %= $station->[2]{sched_arr_dt}->strftime('%H:%M') % } </span> % if ($at_startstop or $within) { @@ -259,6 +282,12 @@ % else { <span style="color: #808080;"><%= $station->[0] %></span> % } + % if ($station->[2]{isAdditional}) { + <span style="color: #808080;">⊕</span> + % } + % elsif ($station->[2]{isCancelled}) { + <span style="color: #808080;">∅</span> + % } % if ($journey->{edited} & 0x0010) { ∗ % } @@ -276,6 +305,7 @@ % $before = 0; % } <br/> + % $i += 1; % } </td> </tr> @@ -293,62 +323,99 @@ </div> </div> % if (not stash('readonly')) { - % if (stash('with_share')) { - <div class="row"> - <div class="col s12 m6 l6"> - </div> - <div class="col s12 m6 l6 center-align"> + <div class="row"> + <div class="col s12 m6 l4 center-align"> + <a class="btn waves-effect waves-light" href="<%= url_for('journey', id => $journey->{id}, format => 'json' ) %>"> + <i class="material-icons left" aria-hidden="true">file_download</i> + %= L('journey.export') + </a> + </div> + <div class="col s12 m6 l4 center-align"> + %= form_for '/journey/edit' => (method => 'POST') => begin + %= hidden_field 'journey_id' => param('journey_id') + <button class="btn waves-effect waves-light" type="submit" name="action" value="edit"> + <i class="material-icons left" aria-hidden="true">edit</i> + %= L('journey.edit') + </button> + %= end + </div> + <div class="col s12 m6 l4 center-align"> + % if (stash('with_share')) { <a class="btn waves-effect waves-light action-share" % if (stash('journey_visibility') eq 'public') { data-url="<%= url_for('public_journey', name => current_user()->{name}, id => $journey->{id} )->to_abs->scheme('https'); %>" % } % else { - data-url="<%= url_for('public_journey', name => current_user()->{name}, id => $journey->{id} )->to_abs->scheme('https'); %>?token=<%= $journey->{from_eva} %>-<%= $journey->{checkin_ts} % 337 %>-<%= $journey->{sched_dep_ts} %>" + data-url="<%= url_for('public_journey', name => current_user()->{name}, id => $journey->{id} )->to_abs->scheme('https'); %>?token=<%= $journey->{from_eva} %>-<%= $journey->{checkin_ts} % 337 %>-<%= int($journey->{sched_dep_ts}) %>" % } data-text="<%= stash('share_text') %>" > - <i class="material-icons left" aria-hidden="true">share</i> Teilen + <i class="material-icons left" aria-hidden="true">share</i> + %= L('journey.share') </a> - </div> - </div> - % } - <div class="row hide-on-small-only"> - <div class="col s12 m6 l6 center-align"> - <a class="waves-effect waves-light red btn action-delete" - data-id="<%= $journey->{id} %>" - data-checkin="<%= $journey->{checkin}->epoch %>" - data-checkout="<%= $journey->{checkout}->epoch %>"> - <i class="material-icons left">delete_forever</i> - Löschen - </a> - </div> - <div class="col s12 m6 l6 center-align"> - %= form_for '/journey/edit' => (method => 'POST') => begin - %= hidden_field 'journey_id' => param('journey_id') - <button class="btn waves-effect waves-light" type="submit" name="action" value="edit"> - <i class="material-icons left" aria-hidden="true">edit</i> - Bearbeiten - </button> - %= end + % } </div> </div> - <div class="row hide-on-med-and-up"> - <div class="col s12 m6 l6 center-align"> - %= form_for '/journey/edit' => (method => 'POST') => begin - %= hidden_field 'journey_id' => param('journey_id') - <button class="btn waves-effect waves-light" type="submit" name="action" value="edit"> - <i class="material-icons left" aria-hidden="true">edit</i> - Bearbeiten - </button> - %= end + % if ($journey->{polyline} or $journey->{km_route} > 0.1) { + <h2><%= L('journey.map-data') %> + % if ($journey->{edited} & 0x0040) { + ∗ + % } + </h2> + <div class="row"> + <div class="col s12 m6 l6 center-align"> + <a class="btn waves-effect waves-light" href="<%= url_for('polyline_download', id => $journey->{id}, format => 'json' ) %>"> + <i class="material-icons left" aria-label="<%= L('journey.map.download') %>">file_download</i> + JSON + </a> + </div> + <div class="col s12 m6 l6 center-align"> + <a class="btn waves-effect waves-light" href="<%= url_for('polyline_download', id => $journey->{id}, format => 'gpx' ) %>"> + <i class="material-icons left" aria-label="<%= L('journey.map.download') %>">file_download</i> + GPX + </a> + </div> </div> - <div class="col s12 m6 l6 center-align" style="margin-top: 1em;"> + <div class="row"> + <div class="col s12"> + %= L('journey.map.info.download') + %= L('journey.map.info.upload') + </div> + </div> + %= form_for '/polyline/set' => (method => 'post', enctype => 'multipart/form-data') => begin + %= csrf_field + %= hidden_field id => $journey->{id} + <div class="row"> + <div class="col s12"> + <div class="file-field input-field"> + <div class="btn"> + <span><i class="material-icons left" aria-label="<%= L('journey.map.upload') %>">file_upload</i> GPX</span> + <input type="file"> + </div> + <div class="file-path-wrapper"> + <input class="file-path validate" type="text" name="file"> + </div> + %= file_field 'file' + </div> + </div> + <div class="col s12 m6 center-align"> + %= submit_button L('journey.map.upload-full'), class => 'waves-effect waves-light btn', name => 'upload-full' + </div> + <div class="col s12 m6 center-align"> + %= submit_button L('journey.map.upload-partial'), class => 'waves-effect waves-light btn', name => 'upload-partial' + </div> + </div> + %= end + % } + <h2><%= L('journey.danger') %></h2> + <div class="row"> + <div class="col s12 m12 l12 center-align" style="margin-top: 1em;"> <a class="waves-effect waves-light red btn action-delete" data-id="<%= $journey->{id} %>" data-checkin="<%= $journey->{checkin}->epoch %>" data-checkout="<%= $journey->{checkout}->epoch %>"> <i class="material-icons left" aria-hidden="true">delete_forever</i> - Löschen + %= L('journey.delete') </a> </div> </div> diff --git a/templates/landingpage.html.ep b/templates/landingpage.html.ep index 5ca0e9e..ba0433f 100644 --- a/templates/landingpage.html.ep +++ b/templates/landingpage.html.ep @@ -55,22 +55,22 @@ %= 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> + <span class="card-title"><%= L('landingpage.greeting-prefix') %> <%= $user->{name} %><%= L('landingpage.greeting-suffix') %></span> + <p><%= L('landingpage.not-checked-in') %>.</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> + <a class="btn waves-effect waves-light btn-flat request"><%= L('landingpage.stop-geosearch') %></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> + <label for="station"><%= L('landingpage.manual-stop-entry') %></label> </div> </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 + %= L('landingpage.departures') </button> </div> </div> @@ -97,45 +97,36 @@ </div> </div> % } - <h2 style="margin-left: 0.75rem;">Letzte Fahrten</h2> - %= include '_history_trains', date_format => '%d.%m.%Y', journeys => [journeys->get(uid => $user->{id}, limit => 5, with_datetime => 1)]; + <h2 style="margin-left: 0.75rem;"><%= L('landingpage.latest-trips') %></h2> + %= include '_history_trains', date_format => L('landingpage.date-format'), 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 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. + %= L('landingpage.about') </p> <p> - Die Idee dazu kommt von <a - href="https://traewelling.de/">Träwelling</a>. + %= L('landingpage.traewelling.pre') + <a href="https://traewelling.de/">Träwelling</a> + %= L('landingpage.traewelling.post') </p> <p> - Features: + %= L('landingpage.features') <ul> - <li>Protokoll von Fahrplan- und Echtzeitdaten an Start- und - Zielbahnhof</li> - <li>Teilen von aktuellen und vergangenen Fahrten mit anderen Personen</li> - <li>Web-Hooks und <a href="/api">API</a> zum automatisierten Einchecken und Auslesen des aktuellen Status</li> - <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><%= L('landingpage.features.log') %></li> + <li><%= L('landingpage.features.share') %></li> + <li><%= L('landingpage.features.api-pre') %> <a href="/api"><%= L('landingpage.features.api-link') %></a> <%= L('landingpage.features.api-post') %></li> + <li><%= L('landingpage.features.stats') %></li> + <li><%= L('landingpage.features.passenger-rights') %></li> + <li><%= L('landingpage.features.public') %></li> </ul> </p> <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. Wer mag, kann auch den - <a href="https://finalrewind.org/projects/travelynx">Quelltext</a> - laden und eine eigene Instanz aufsetzen. + %= L('landingpage.disclaimer.lead') + %= L('landingpage.disclaimer.source-pre') + <a href="https://finalrewind.org/projects/travelynx"><%= L('landingpage.disclaimer.source-link') %></a> + %= L('landingpage.disclaimer.source-post') </p> </div> </div> @@ -144,9 +135,9 @@ </div> <div class="col s10 m10 l6 center-align"> % if (not app->config->{registration}{disabled}) { - <a href="/register" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">add</i>Registrieren</a> + <a href="/register" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">add</i><%= L('button.register') %></a> % } - <a href="/login" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">account_circle</i>Anmelden</a> + <a href="/login" class="waves-effect waves-light btn"><i class="material-icons left" aria-hidden="true">account_circle</i><%= L('button.login') %></a> </div> <div class="col s1 m1 l3"> </div> diff --git a/templates/language.html.ep b/templates/language.html.ep new file mode 100644 index 0000000..0d14fe6 --- /dev/null +++ b/templates/language.html.ep @@ -0,0 +1,76 @@ +<h1><%= L('language.language') %></h1> +%= form_for '/account/language' => (method => 'POST') => begin + %= csrf_field + <div class="row"> + <div class="input-field col s12"> + <div> + <label> + %= radio_button language => 'none' + <span>–: <%= L('language.browser-default') %></span> + </label> + </div> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + <div> + <label> + %= radio_button language => 'de-DE' + <span>de-DE: Deutsch (hochdeutsch)</span> + </label> + </div> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + <div> + <label> + %= radio_button language => 'en-GB' + <span>en-GB: English (Great Britain)</span> + </label> + </div> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + <div> + <label> + %= radio_button language => 'fr-FR' + <span>fr-FR: français</span> + </label> + </div> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + <div> + <label> + %= radio_button language => 'hu-HU' + <span>hu-HU: magyar</span> + </label> + </div> + </div> + </div> + <div class="row"> + <div class="input-field col s12"> + <div> + <label> + %= radio_button language => 'pl-PL' + <span>pl-PL: Polski</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/layouts/default.html.ep b/templates/layouts/default.html.ep index e67f99c..81dfea0 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 = 'v92'; # asset version + % my $av = 'v99'; # 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"> @@ -90,7 +90,7 @@ </div> </li> % if ($acc) { - <li class="<%= navbar_class('/history') %>"><a href='/history' title="Vergangene Zugfahrten"><i class="material-icons" aria-label="Vergangene Zugfahrten">history</i></a></li> + <li class="<%= navbar_class('/history') %>"><a href='/history' title="Vergangene Fahrten"><i class="material-icons" aria-label="Vergangene Fahrten">history</i></a></li> <li class="<%= navbar_class('/account') %>"><a href="/account" title="Account"><i class="material-icons" aria-label="Account"><%= $acc->{notifications} ? 'notifications' : 'account_circle' %></i></a></li> % } % else { @@ -131,21 +131,21 @@ <div class="col s12 center-align grey-text"> <a href="/about">travelynx</a> v<%= $version // '???' %> <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span> - <a href="/impressum">Impressum</a> + <a href="/impressum"><%= L('footer.imprint') %></a> <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span> - <a href="/impressum">Datenschutz</a> + <a href="/impressum"><%= L('footer.privacy') %></a> <span style="margin-left: 0.5em; margin-right: 0.5em;">–</span> - <a href="/legend">Legende</a> + <a href="/legend"><%= L('footer.legend') %></a> </div> </div> <div class="row"> <div class="col s12 center-align grey-text config"> - Farbschema: - <a onClick="javascript:setTheme('light')">hell</a> + <%= L('footer.colour-scheme') %>: + <a onClick="javascript:setTheme('light')"><%= L('footer.colour-scheme.light') %></a> · - <a onClick="javascript:setTheme('dark')">dunkel</a> + <a onClick="javascript:setTheme('dark')"><%= L('footer.colour-scheme.dark') %></a> · - <a onClick="javascript:setTheme('default')">automatisch</a> + <a onClick="javascript:setTheme('default')"><%= L('footer.colour-scheme.auto') %></a> </div> </div> </div> diff --git a/templates/login.html.ep b/templates/login.html.ep index 3a9cc1f..21f14d3 100644 --- a/templates/login.html.ep +++ b/templates/login.html.ep @@ -75,7 +75,9 @@ </div> <div class="row"> <div class="col s12 m12 l12"> - Mit der Anmeldung stimmst du den <a href="/tos">Nutzungsbedingungen</a> zu. + %= L('login.accept-tos-pre') + <a href="/tos"><%= L('login.tos') %></a> + %= L('login.accept-tos-post') </div> </div> <div class="row"> @@ -83,7 +85,7 @@ </div> <div class="col s6 m6 l6 center-align"> <button class="btn waves-effect waves-light" type="submit" name="action" value="login"> - Anmelden + %= L('button.login') <i class="material-icons right">send</i> </button> </div> @@ -95,7 +97,7 @@ </div> <div class="col s6 m6 l6 center-align"> <a href="/recover"> - Passwort vergessen + %= L('login.forgot-password') </a> </div> <div class="col s3 m3 l3"> @@ -104,7 +106,7 @@ % if (app->config->{registration}{disabled}) { <div class="row" style="margin-top: 2em;"> <div class="col s12 center-align"> - <em>Diese Instanz erlaubt derzeit keine Registrierung neuer Accounts</em> + <em><%= L('login.registration-disabled') %></em> </div> </div> % } diff --git a/templates/passengerrights.html.ep b/templates/passengerrights.html.ep index c189657..bf9e5de 100644 --- a/templates/passengerrights.html.ep +++ b/templates/passengerrights.html.ep @@ -3,13 +3,13 @@ <div class="col s12"> <p> Ab 60 Minuten Verspätung am Ziel besteht in einigen Fällen ein - Entschädigungsanspruch gegenüber dem Eisenbahnverkehrsunternehmen. + Entschädigungsanspruch gegenüber dem Verkehrsunternehmen. Dieser kann mit dem Fahrgastrechteformular oder online geltend gemacht werden. </p> <p> - Die folgenden Zugfahrten sind wahrscheinliche Kandidaten dafür. - Details zur jeweiligen Zugfahrt sind bereits im Formular eingetragen. + Die folgenden Fahrten sind wahrscheinliche Kandidaten dafür. + Details zur jeweiligen Fahrt sind bereits im Formular eingetragen. </p> </div> </div> @@ -20,7 +20,7 @@ <thead> <tr> <th>Datum</th> - <th>Zug</th> + <th>Fahrt</th> <th>Grund</th> <th>Formular</th> </tr> @@ -79,7 +79,7 @@ <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. + einzureichen. Die folgenden Fahrten sind Kandidaten dafür. Fahrten mit einer Verspätung von 60 Minuten oder mehr werden hier nicht aufgeführt. </p> @@ -92,7 +92,7 @@ <thead> <tr> <th>Datum</th> - <th>Zug</th> + <th>Fahrt</th> <th>Verspätung</th> </tr> </thead> diff --git a/templates/polyline.gpx.ep b/templates/polyline.gpx.ep new file mode 100644 index 0000000..a243926 --- /dev/null +++ b/templates/polyline.gpx.ep @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8"?> +<gpx + xmlns="http://www.topografix.com/GPX/1/1" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd" + version="1.1" + creator="travelynx <%= $version %>"> + <trk> + <name><%= $name %></name> + <trkseg> + % for my $entry (@{$polyline // []}) { + <trkpt lon="<%= $entry->[0] %>" lat="<%= $entry->[1] %>"> + % if ($entry->[2]) { + <name><%= $entry->[2] %></name> + % } + </trkpt> + % } + </trkseg> + </trk> +</gpx> diff --git a/templates/register.html.ep b/templates/register.html.ep index f9a486a..e7064da 100644 --- a/templates/register.html.ep +++ b/templates/register.html.ep @@ -8,27 +8,29 @@ <div class="input-field col l6 m12 s12"> <i class="material-icons prefix">account_circle</i> %= text_field 'user', id => 'account', class => 'validate', required => undef, pattern => '[0-9a-zA-Z_-]+', maxlength => 60, autocomplete => 'username' - <label for="account">Name (alphanumerisch)</label> + <label for="account"><%= L('register.name') %></label> </div> <div class="input-field col l6 m12 s12"> <i class="material-icons prefix">email</i> %= email_field 'email', id => 'email', class => 'validate', required => undef, maxlength => 250 - <label for="email">Mail-Adresse</label> + <label for="email"><%= L('register.mail') %></label> </div> <div class="input-field col l6 m12 s12"> <i class="material-icons prefix">lock</i> %= password_field 'password', id => 'password', class => 'validate', required => undef, minlength => 8, maxlength => 10000, autocomplete => 'new-password' - <label for="password">Passwort</label> + <label for="password"><%= L('register.password') %></label> </div> <div class="input-field col l6 m12 s12"> <i class="material-icons prefix">lock</i> %= password_field 'password2', id => 'password2', class => 'validate', required => undef, minlength => 8, maxlength => 10000, autocomplete => 'new-password' - <label for="password2">Passwort wiederholen</label> + <label for="password2"><%= L('register.repeat-password') %></label> </div> </div> <div class="row"> <div class="col s12 m12 l12"> - Mit deiner Registrierung stimmst du den <a href="/tos">Nutzungsbedingungen</a> zu. + %= L('register.accept-tos-pre') + <a href="/tos"><%= L('register.tos') %></a> + %= L('register.accept-tos-post') </div> </div> <div class="row"> @@ -36,7 +38,7 @@ </div> <div class="col s6 m6 l6 center-align"> <button class="btn waves-effect waves-light" type="submit" name="action" value="register"> - Registrieren + %= L('button.register') <i class="material-icons right">send</i> </button> </div> @@ -47,9 +49,7 @@ <div class="row"> <div class="col s12"> <p> - Nach der Registrierung wird ein für 48 Stunden gültiger - Bestätigungslink an die angegebene Mail-Adresse geschickt. Eine - Anmeldung ist erst nach Bestätigung der Mail-Adresse möglich. + %= L('register.expect-confirmation-link') </p> <p> Die Mail-Adresse wird ausschließlich zur Bestätigung der Anmeldung, diff --git a/templates/select_backend.html.ep b/templates/select_backend.html.ep index e3db44d..999a689 100644 --- a/templates/select_backend.html.ep +++ b/templates/select_backend.html.ep @@ -11,6 +11,20 @@ % if (stash('redirect_to')) { %= hidden_field 'redirect_to' => stash('redirect_to') % } + % if (@{stash('frequent') // []}) { + <div class="row"> + <div class="col s12"> + <h3>Häufig genutzt</h3> + <p style="text-align: justify;"> + Die folgenden Backends wurden innerhalb der letzten vier + Monate für Checkins verwendet. + </p> + </div> + </div> + % for my $backend (@{ stash('frequent') // [] }) { + %= include '_backend_line', user => $user, backend => $backend + % } + % } % if (@{stash('suggestions') // []}) { <div class="row"> <div class="col s12"> @@ -4,6 +4,7 @@ # as necessary. git pull +git submodule update --init if [ "$1" = "with-deps" ]; then mkdir local.new |
